Rails 8.2 Fixes a Subtle ActiveJob Bug You Might Not Know You Have

railsactivejobactiverecord

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:

  1. Job fails with RecordNotFound: The job runs before the record is visible to other database connections
  2. Job processes stale data: The job reads uncommitted data that gets rolled back
  3. 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.

See the commit and PR #55788 for the full discussion.