Updated June 2026. Tested on Laravel 13 and PHP 8.4. Part of the Techalyst queue series.
A queued job is not normal code. It gets serialized, stored, shipped to another process, unserialized, and only then run, perhaps instantly, perhaps hours later, possibly on a different machine, possibly more than once. If you design jobs as if they run immediately in the current request, they will betray you. The single most important habit is to make jobs self-contained.
The problem: state changes between dispatch and run
You cannot know when a job will actually run. The system's state at run time may be very different from when you dispatched it. So a job that reads "the current state of things" inside handle() is reading a different world from the one the user acted on.
Consider a deploy job.
DeployProject::dispatch($site, $site->lastCommitHash());
class DeployProject implements ShouldQueue
{
public function __construct(
public Site $site,
public string $commitHash,
) {}
public function handle(): void
{
// deploy exactly $this->commitHash
}
}
We captured the commit hash at dispatch and carry it into the job. We could instead have called $this->site->lastCommitHash() inside handle(), but watch what that would do.
Why reading state at run time is a bug here
Say a user changes the "Log In" button to green, hits deploy, then changes it to blue. They expect the deployment that is running to produce the green button, because that is what existed when they clicked deploy.
If the job read lastCommitHash() at run time, and another commit (the blue button) landed before a worker picked the job up, the deploy would ship the blue button. The user gets a result they never asked for. By capturing the hash at dispatch, the job deploys exactly what the user intended, no matter how long it waits or what changes afterwards.
The rule: decide what should be frozen in time
For every piece of data a job depends on, ask one question: should this be frozen at the moment of dispatch, or should it reflect the latest state when the job runs?
- If the job's purpose is tied to a moment, a user action, an amount, a snapshot, then capture it at dispatch and pass it into the constructor. It becomes part of the serialized payload and cannot drift.
- If the job's purpose is genuinely "do this to whatever the current state is", then reading at run time is correct.
Most of the time, the first answer is the right one, and getting it wrong produces bugs that are maddening to reproduce because they depend on timing.
A note on models and SerializesModels
There is a subtlety with Eloquent models. The SerializesModels trait stores only a model's id and re-fetches a fresh copy when the job runs. That is great for avoiding stale model data and huge payloads, but it means model attributes reflect the database at run time, not at dispatch. So if a specific attribute value must be frozen, do not rely on the model carrying it, pass that value as its own scalar argument, exactly like the commit hash above. Let the model give you identity and fresh related data; pass scalars for anything that must not change.
Wrapping up
Because a queued job runs at an unknown time, on another machine, maybe more than once, the most reliable design is a self-contained job: capture the data the job depends on at dispatch and pass it into the constructor, rather than reading the current state inside handle(). Freeze what is tied to a moment, read fresh only what genuinely should be current, and remember that SerializesModels reloads models at run time. Get this right and a whole class of timing bugs simply cannot happen.
More in the series: how Laravel prepares a job for the queue and dispatching unique jobs. Questions welcome below.
All comments ()
No comments yet
Be the first to leave a comment on this post.