Rails 8.2 Fixes a Subtle ActiveJob Bug You Might Not Know You Have
Rails 8.2 changes how ActiveJob interacts with database transactions, fixing a subtle bug that has bitten many developers. Jobs enqueued inside a transaction now wait until the transaction commits before being dispatched.
The Problem
Consider this common pattern:
class OrdersController < ApplicationController
def create
ActiveRecord::Base.transaction do
@order = Order.create!(order_params)
OrderConfirmationJob.perform_later(@order)
end
end
end
Looks reasonable, right? But there’s a race condition. The job might execute before the transaction commits. When the job runs:
class OrderConfirmationJob < ApplicationJob
def perform(order)
# This might fail! The order might not exist yet in the database
OrderMailer.confirmation(order).deliver_now
end
end
Even worse, if the transaction rolls back, the job still runs against a record that no longer exists.
What Changed
In Rails 8.2, config.active_job.enqueue_after_transaction_commit defaults to true. Jobs enqueued inside a transaction are now held until after the transaction commits.
ActiveRecord::Base.transaction do
@order = Order.create!(order_params)
OrderConfirmationJob.perform_later(@order) # Job is held, not enqueued yet
end
# Transaction commits here
# NOW the job is dispatched to the queue
If the transaction rolls back, the job is never enqueued.
Why This Matters
This fixes several common issues:
- Job fails with RecordNotFound: The job runs before the record is visible to other database connections
- Job processes stale data: The job reads uncommitted data that gets rolled back
- Duplicate work: Transaction retries cause multiple jobs to be enqueued
How to Enable It
New Rails 8.2 apps
It’s enabled by default. Nothing to do.
Upgrading to Rails 8.2
Add to config/initializers/new_framework_defaults_8_2.rb:
Rails.application.config.active_job.enqueue_after_transaction_commit = true
Or set it in config/application.rb:
config.active_job.enqueue_after_transaction_commit = true
Per-job override
If a specific job should be enqueued immediately (perhaps it doesn’t depend on the transaction):
class NotificationJob < ApplicationJob
self.enqueue_after_transaction_commit = false
def perform(message)
# This job will be enqueued immediately, even inside a transaction
end
end
Things to Watch Out For
Jobs outside transactions
Jobs enqueued outside of a transaction are dispatched immediately, as before:
# No transaction, job enqueues immediately
SomeJob.perform_later(data)
Nested transactions
The job waits for the outermost transaction to commit:
ActiveRecord::Base.transaction do
User.create!(...)
ActiveRecord::Base.transaction do
Order.create!(...)
OrderJob.perform_later(@order) # Held until outer transaction commits
end
end
# Job dispatched here
Return value timing
perform_later still returns the job instance immediately, even though the actual enqueue is deferred:
ActiveRecord::Base.transaction do
job = MyJob.perform_later(data)
job.successfully_enqueued? # true (optimistically)
end
If the actual enqueue fails later, the job’s status is updated, but this happens after the transaction.
Wrapping Up
This is a sensible default that prevents a common class of bugs. If you’ve been manually using after_commit callbacks to enqueue jobs, you can now simplify your code.
For existing apps, test thoroughly before enabling, as the timing change might affect job ordering assumptions.