Substack iconSubstackSep 6, 2026 ~7 min source read

What Actually Happens When You Call perform_later

A step-by-step explanation of how Rails Active Job constructs work, hands it to a queue adapter, and why an enqueued job can sit ready without any worker ever running perform.

What Actually Happens When You Call perform_later

Share this story

Send the public story page.

Useful takeaways from this story.

perform_later builds a job object, assigns identity and arguments, then hands a serialized description to a queue adapter — it does not move the current stack or local state to a worker process.

The adapter and backend define durability and execution semantics: different adapters (async, Solid Queue, cloud providers) store and schedule work differently, so an enqueued job may remain ready without being executed.

# What actually happens when you call perform_later

This explains the concrete lifecycle of an Active Job enqueued with perform_later, why an enqueued job might never run, and what pieces determine whether work reaches a worker.

perform_later does not suspend and resume your request

perform_now is different: it executes perform in the current process. perform_later prepares a description of work and passes it to an adapter for handoff.

The adapter is the boundary: Active Job stops there

Serialization: what travels to the worker

Different adapters provide different guarantees. The async adapter keeps work in process memory, so a restart loses those jobs. Solid Queue persists database rows. Both implement the same Active Job API, but they produce different durability and scheduling semantics.

Solid Queue example: it sets scheduled_at to now when preparing a stored job, stores the serialized payload in a solid_queue_jobs record, and prepares an execution record according to its scheduling and concurrency rules. Ready executions become visible to workers that match the queue configuration. When a worker claims a ready execution, Solid Queue merges the database job ID back into the serialized payload as provider_job_id before handing it to Active Job.

Those implementation details explain why a dashboard may show a job in a ready state while an error tracker has no entries, the retry count is zero, and the job never ran: the job has been accepted and stored by the backend, but no worker matching that queue claimed or executed the ready execution.

Common operational pitfalls

  • Treat queue names as deployment configuration: worker routing and queue configuration determine which processes can claim executions.
  • Adding retry logic in job code without confirming whether perform ever started can lead to changes that don't affect the observed problem. A missing worker or mismatched queue configuration will keep an enqueued job in ready state regardless of retry_on changes.
  • Passing Active Record objects to perform_later produces GlobalIDs (identifiers), not snapshots. Workers load the record when they run, so they see the record's state at execution time, not at enqueue time.

Practical next steps if a job is enqueued but never runs

Confirm which adapter is in use, check backend storage for a provider_job_id or execution record, verify worker queue configurations, and inspect whether enqueue callbacks or adapter errors raised during handoff. Those checks identify whether the problem is enqueue-time (adapter handoff) or runtime (no worker claimed execution).

More context around this story.

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

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