How Laravel Jobs Really Work: Serialization and the Life of a Queued Job
dispatch(new SendWelcomeEmail($user)) looks like an ordinary method call. It isn't one. Understanding what actually happens between that line and the job running, possibly minutes later on a completely different server, explains a whole category of "why doesn't my job see the change I just made" bugs.
A job is data, not code-in-waiting
When you dispatch a job, Laravel doesn't keep your SendWelcomeEmail object sitting in memory somewhere, patiently waiting its turn. It serializes the object — turns it into a string representation of its public and protected properties — and stores that string in whatever queue backend you're using: a database row, a Redis list entry, an SQS message body.
class SendWelcomeEmail implements ShouldQueue
{
use Queueable;
public function __construct(public User $user) {}
public function handle(): void
{
Mail::to($this->user)->send(new WelcomeMail($this->user));
}
}
That public User $user property is where the interesting part happens. PHP's native serialization of an Eloquent model would try to serialize every loaded relationship, every attribute, the whole object graph — bloated, fragile, and liable to break the moment your model's internal structure changes between dispatch and execution.
SerializesModels: storing a reference, not a snapshot
The Queueable trait pulls in Illuminate\Queue\SerializesModels, which intercepts serialization for any Eloquent model property. Instead of serializing the model's data, it serializes just enough to identify it: the class name and the primary key (plus the connection name, for multi-database setups).
// What actually gets serialized, conceptually:
[
'class' => App\Models\User::class,
'id' => 482,
'connection' => 'mysql',
]
When a queue worker picks up the job and unserializes it, SerializesModels runs in reverse: it sees that reference and issues User::find(482) to fetch a fresh copy of the model from the database, right there, right before handle() runs.
This single fact explains a lot:
- Stale data isn't stale — it's fresh, from the wrong moment. If you dispatch a job, then update the user's email before the job runs, the job sees the new email, because it re-fetches from the database rather than using whatever the model looked like at dispatch time.
- A deleted model between dispatch and execution throws a
ModelNotFoundException, not a silently stale object — because the re-fetch fails outright. - Unsaved changes on the model at dispatch time are lost. If you mutate an attribute on
$userwithout callingsave(), then dispatch a job holding$user, the job's re-fetched copy won't have that unsaved change. Only what's actually in the database survives the round trip.
The full lifecycle
- Dispatch: your job object is serialized (with model properties reduced to references) and pushed onto the configured queue connection.
- Storage: the serialized payload sits in the queue backend — a
jobstable row, a Redis entry — as inert data, not a live PHP object. - Pop: a queue worker process, running
php artisan queue:work, pulls the next payload off the queue. - Unserialize: the worker reconstructs your job object.
SerializesModelsre-fetches every model property from the database at this exact moment. - Handle:
handle()runs with those freshly loaded models. - Acknowledge: on success, the job is deleted from the queue. On failure, it's released back for retry (up to your configured
tries) or moved to the failed jobs table.
Why this matters for how you write jobs
Because a job might run seconds or hours after dispatch, on a worker process that shares nothing with the request that created it, write jobs assuming the world has moved on:
public function handle(): void
{
// Don't assume $this->user still has the role it had at dispatch time —
// re-check anything that matters for correctness.
if (! $this->user->hasRole('subscriber')) {
return;
}
Mail::to($this->user)->send(new WelcomeMail($this->user));
}
That defensive re-check isn't paranoia — it's a direct consequence of what serialization actually does. The job doesn't carry a memory of the world at dispatch time; it carries an address to look the world up again.