After-commit enqueue callbacks leave a recovery decision
Rails moved enqueue callbacks behind transaction commit when deferral is enabled. Plan recovery when committed data remains but a job cannot reach its queue.
Moving an enqueue callback after commit puts it around the queue interaction it is meant to observe. It does not make a database commit and an external queue write one indivisible operation. Applications still need to decide what happens when data is committed but the job cannot be delivered. The right recovery depends on the consequence of missing that job.
This November 2026 retrospective concerns the Rails report of 9 May 2025 and the change merged on 5 May. The report highlights deferral of around_enqueue callbacks when enqueue_after_transaction_commit is enabled. These are dates of a merged change and a weekly announcement, not proof that all Rails versions shipped that behavior during the research week.
The wrapper needs to surround the real attempt
The pull request describes a contributor's use of around_enqueue to catch Sidekiq connectivity errors and save undelivered payloads in a database for possible manual recovery. That is the contributor's account, not an AL+ implementation. The reported problem was that callbacks ran when perform_later was called, while the adapter interaction could happen later, after commit.
A wrapper executing before that later interaction cannot catch an exception that occurs after the wrapper has finished. Moving the callback timing therefore matters even when the job itself was already deferred. “The enqueue happens after commit” and “the enqueue callback surrounds the eventual attempt” are different claims. The change addresses the second relationship under the enabled setting.
The merged deferral module registers work through ActiveRecord.after_all_transactions_commit. The matching enqueue implementation runs enqueue callbacks around the internal adapter call. Read those files together: they explain how the deferred path reaches callbacks at the later time, rather than treating the weekly summary as a complete API contract.
The setting is a condition of this discussion. Do not generalize the behavior to jobs configured for immediate enqueueing. Likewise, a normal job execution callback has a different lifecycle from an enqueue callback. A worker can only handle a job it receives; recovery for a payload that never entered the queue must begin elsewhere.
Commit separates two kinds of success
Consider an illustrative order workflow. A transaction records an accepted order and requests a job to notify another system. After the transaction commits, the adapter attempts to submit the job. If that attempt fails, the order can remain validly stored while the notification is absent. Deferring the attempt prevents it from depending on uncommitted data, but leaves this later failure possible.
The request handler also needs to understand what its immediate return value means. In the merged implementation, the deferred path can return a job instance before the adapter has completed its eventual attempt. That instance cannot be read as a durable receipt from the external queue. Success at scheduling a future attempt and success at handing off the payload are separate observations.
A callback that catches a connection error and writes a recovery record can cover a useful failure path. It does not cover every possible process failure. If the process disappears after commit but before the recovery record is written, neither the job nor that record is guaranteed to exist. The timing fix alone cannot close that window.
The outcome of an enqueue attempt may also be uncertain rather than clearly failed. A connection can break after an external service accepted a request but before the application observed confirmation. Retrying then risks duplicate work. The application needs a policy for identifying the business operation and tolerating or preventing duplicate effects; callback placement does not create exactly-once delivery.
A recovery record also needs enough information to distinguish an unsent operation from one already completed. Retaining only an exception message may help diagnosis without making replay safe. Decide what identifies the operation and how an operator checks its state before another attempt. This is an application responsibility beyond the callback timing change.
Durable intent has an operating cost
For a consequential operation, an outbox is one possible design: store the intention to send work alongside the business change in the same database transaction, then let a dispatcher deliver pending intentions. This describes a proposed architecture. The Rails change does not automatically add such a table or dispatcher, and the article claims no implementation or experiment.
That design moves the question from whether the application remembered the intention to whether the dispatcher can make progress. Pending records need observation, retries need a policy and duplicate delivery still needs handling. The team must also decide what happens when a record can never be delivered. Durability is useful only if stalled work becomes visible to someone responsible.
The strongest counterargument is a low-stakes, reconstructible job. A periodic refresh or optional notification may not justify an outbox's extra data and operating duties. A documented retry or reconciliation process can be enough if the business accepts its limits. The requirement is an explicit choice about loss, delay and duplicates, rather than a universal architecture.
For custom software development, trace one consequential job from the committed record to the queue receipt and then to its effect. Name the owner of recovery at each gap. Keep the after-commit callback improvement for the lifecycle problem it solves, and make durable intent a separate decision whose cost matches the obligation to the customer.
Paul