Substack iconSubstackSep 20, 2026 ~7 min source read

When perform_later Lies: How a Committed Transaction Can Leave a Missing Job

Deferring Active Job enqueueing until after commit orders two durable writes but does not make them atomic. A Solid Queue failure that happens after the primary DB commit can leave a durable order but no queued work.

Your Transaction Committed. Did the Job Reach the Queue?

Share this story

Send the public story page.

Useful takeaways from this story.

after_commit / enqueue_after_transaction_commit controls ordering but not atomicity: the primary DB can commit while the queue insert later fails.

Use observable evidence (provider_job_id, solid_queue_jobs row, enqueue.active_job events), idempotent writes, and architectural patterns (shared storage or outbox) to change or detect the guarantee.

# The symptom: a 500 but the order exists and no job is queued

A request fails with SolidQueue::Job::EnqueueError. The controller shows a 500 and the team expects the order transaction to have rolled back. Instead, the order row is present in the primary database, there is no FulfillOrderJob in the queue database, and retrying the request hits the order's uniqueness constraint.

The application code is short and straightforward: an Order.transaction creates the order and calls FulfillOrderJob.perform_later(order.id), with the job configured to enqueue_after_transaction_commit. Yet the exception did not roll back the order because the queue write happened after the primary commit and then failed.

# Why this happens

Deferring a job until after commit guarantees only order: the primary DB commit happens before Rails asks the queue adapter to persist the job. It does not make the two writes a single atomic operation across databases.

When enqueue_after_transaction_commit is active, perform_later returns a job object immediately while the transaction is still open. Rails registers an after-commit hook that will run the real adapter enqueue only once the transaction commits. During the transaction the job object can show successfully_enqueued? = true, but that reflects Rails' deferred path, not backend acceptance.

A runnable repro on Rails 8.1.3 and Solid Queue 1.4.0 shows the intermediate state. Inside an Order.transaction:

  • Order.create! writes the row.
  • FulfillOrderJob.perform_later(order.id) returns a job with successfully_enqueued? => true and provider_job_id => nil.
  • SolidQueue::Job.count is still 0 until the transaction commits and the deferred enqueue runs.

# Concrete signals of a successful enqueue

  • provider_job_id assigned by Solid Queue
  • a row in the solid_queue_jobs table (or equivalent queue table)
  • the enqueue.active_job event carrying no exception

Relying on successfully_enqueued? alone during a transaction is misleading because it indicates intent rather than backend acceptance.

# Practical mitigations and operational steps

  • Make application writes idempotent so retrying a controller action that produced a 500 won't collide badly. Uniqueness constraints should be paired with idempotent retry behavior.
  • Treat provider_job_id and the queue table row as the ground truth for whether the queue accepted work. Instrument enqueue.active_job events and capture exception_object when enqueue failures occur.
  • If you need stronger cross-system guarantees, change the architecture: shared storage or an outbox pattern moves both writes into a single durable handoff or into a single transactional authority so you don't rely on a post-commit callback for the second durable write.
  • Add monitoring and alerting on enqueue failures, and track cases where the request returned an error but the primary DB shows a committed write.

# Bottom line

after_commit ensures order but not atomicity between two different durable systems. A post-commit queue insert can fail after the primary commit, leaving a durable record without corresponding queued work. Detecting and handling this requires different signals, idempotent operations, and, when necessary, architectural changes such as an outbox or shared storage.

More context around this story.

What Actually Happens When You Call perform_later
Substack iconSubstackSep 6, 2026

What Actually Happens When You Call perform_later

Originally appeared on Rails Revelry . The request log says the job was enqueued. The customer’s CRM record still has not changed. Enqueued SyncCustomerJob (Job ID: 7f9...) to SolidQueue(crm_sync) The queue dashboard shows the job sitting in Solid Queue’s ready state. The error tracker has nothing from SyncCustomerJob

The Job Was Enqueued by Older Code
Substack iconSubstackSep 27, 2026

The Job Was Enqueued by Older Code

Originally appeared on Rails Revelry . What happens to the jobs already in the queue when I rename their class? The code change looks straightforward: rename the file, update the callers, and fix the tests. But a job enqueued before the deploy might wait hours or days before a worker picks it up. By then, the old class

Loading more related stories...

Keep reading in the app

Open the app view to save this story, compare related coverage, and continue from the same source.

Open in app