skip to content

In a Laravel queued job, what does SerializesModels store for an Eloquent model passed to the constructor, and what happens on the worker?

level: middleimportance: should knowfreq 52%

answer

  1. an identifier, not the attributes
  2. ModelIdentifier: class, key, relations, connection
  3. firstOrFail on the write connection
  4. unsaved changes do not travel
  5. DeleteWhenMissingModels for deleted rows

basics

~20 s

SerializesModels stores only a model identifier — class, primary key, loaded relation names and connection. On the worker the model is re-queried from the database, so the job sees current data, loses unsaved changes, and fails if the row was deleted.

solid answer

~40 s

When the job is serialized, `SerializesModels` replaces each Eloquent model property with a `ModelIdentifier` holding the class, the key, the names of loaded relations and the connection; a collection stores its keys. On the worker, the trait re-runs a query for each identifier — without global scopes, on the write connection — calls `firstOrFail()` and then `loadMissing()` for the recorded relations. So the job works on the row as it is **now**, not as it was at dispatch: unsaved attribute changes are gone, relation constraints applied before dispatch are gone, and a hard-deleted row throws `ModelNotFoundException`, which marks the job failed without retrying. `#[DeleteWhenMissingModels]` makes Laravel discard such a job quietly instead. `withoutRelations()` or `#[WithoutRelations]` keeps relations out of the payload.

code

php · 26 lines
php
<?php

namespace App\Jobs;

use App\Models\Photo;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\Attributes\DeleteWhenMissingModels;
use Illuminate\Queue\Attributes\WithoutRelations;

#[DeleteWhenMissingModels]
class RefreshThumbnail implements ShouldQueue
{
    use Queueable;

    public function __construct(
        #[WithoutRelations]
        public Photo $photo,
        public int $width,
    ) {}

    public function handle(): void
    {
        // $this->photo was re-fetched from the database on the worker
    }
}

go deeper

for a junior

Remember that only the model's class and key are queued and the worker loads the model again from the database.

for a middle

Explain the ModelIdentifier fields, the firstOrFail restore, the unsaved-changes trap and what DeleteWhenMissingModels changes.

for a senior

Decide per job whether it needs current state or dispatch-time values, and link ModelNotFoundException failures to transaction timing.

for a principal

Set conventions for job payloads — models versus scalars, relation stripping — so payload size and stale-data bugs stay predictable.

## Why models are not serialized whole A queued job crosses a process boundary: the web request builds it, a queue driver stores it, and a worker rebuilds it minutes or hours later. Serializing an Eloquent model whole would copy every attribute and every loaded relation into the payload, making it large and handing the worker a stale snapshot. `Illuminate\Queue\SerializesModels`, included in the Laravel 13 `Queueable` trait, avoids both. ## What goes into the payload When the job is serialized, the trait walks the job's properties and swaps each model for an `Illuminate\Contracts\Database\ModelIdentifier`: - **class** — the model class name; - **id** — the primary key, or an array of keys for an Eloquent collection; - **relations** — the names of relations that were loaded, not their rows; - **connection** — the database connection the model came from. Scalars, arrays of scalars and other serializable values are kept as they are. `$model->withoutRelations()`, `$model->withoutRelation('comments')`, or the `#[WithoutRelations]` attribute on a promoted property or on the whole class leave relation names out, shrinking the payload and the work the worker does. ## What happens on the worker 1. The worker unserializes the job, and the trait restores each identifier. 2. For a single model it builds `newQueryForRestoration($id)` — the model's query **without global scopes** — switches it to the **write PDO** so a lagging read replica cannot hide a fresh row, and calls `firstOrFail()`. 3. It calls `loadMissing()` with the recorded relation names, so relations come back **in full**; any `where` you applied when eager-loading before dispatch is not reapplied. 4. For a collection it runs one query for all keys, keeps the original order, silently drops keys that no longer exist, and loads the recorded relations. Laravel 13 made collections restore eager-loaded relations too; the queue docs page still says they do not, but the upgrade guide and the source agree that they do. ## The consequences interviewers probe | Situation at run time | What the job sees | |---|---| | Attribute changed and saved after dispatch | the new value | | Attribute changed but never saved before dispatch | the database value; the change is lost | | Row soft-deleted | the model, because restoration skips global scopes | | Row hard-deleted | `ModelNotFoundException`, the job is failed | | Relation constrained before dispatch | the whole relation, unconstrained | The missing-model case matters most. The exception is thrown while the job is being unserialized, before `handle()` runs, and the queue handler marks the job **failed immediately**, without using its remaining tries. If a missing model simply means the work is moot — a notification about a deleted comment — put `#[DeleteWhenMissingModels]` on the class (or set `public $deleteWhenMissingModels = true;`), and the job is deleted without an exception. ## Payload size and relation stripping Relation names are cheap, but reloading relations is not. A job constructed with a `Podcast` that had `comments` eager-loaded will reload **every** comment on the worker, even if the controller loaded only the latest ten. Three tools control this: - `$podcast->withoutRelations()` returns a copy with no loaded relations, to assign in the constructor; - `$podcast->withoutRelation('comments')` drops one relation and keeps the rest; - `#[WithoutRelations]` from `Illuminate\Queue\Attributes` does the same declaratively, on a promoted constructor property or on the whole job class. Stripping relations also avoids a subtle cost for collections: a list of 500 models with relations reloads those relations for all 500 on the worker. Loading exactly what the job needs inside `handle()` keeps both the payload and the worker's queries predictable. ## Designing job data with this in mind - Pass the model when the job should act on current state, such as re-rendering a thumbnail or syncing a record to a search index. - Pass scalar values when the job must use dispatch-time values, such as the price a customer agreed to or the old email address for a change notice. - Save the model before dispatching; an unsaved model has no key to store. - Re-apply relation constraints inside `handle()` if the job needs a subset. - Remember that `dispatchSync()` and the `sync` connection also round-trip through a payload, so the same rules apply in tests and inline runs. A model is a pointer to a row, not a copy of it; the job decides at run time what that row says.

  • Why does Laravel restore a queued job's models through the write connection rather than a read replica?
    A job is often dispatched right after the row was created or changed. With read/write splitting, a replica may not have that change yet, so a restore on the read connection could miss the row or see old values. `SerializesModels` calls `useWritePdo()` on the restoration query to read from the primary.
  • How do you make a Laravel job use the price a customer saw at checkout rather than today's price?
    Pass the value itself — for example `public int $priceInCents` — instead of relying on the model. SerializesModels re-fetches models, so an attribute read inside `handle()` reflects the database at run time, not at dispatch.

A coat-check ticket: the ticket names which coat and where it hangs, not the coat itself. You get whatever is on that hook when you come back, and if the coat was taken away the ticket fails.

saying these in an interview costs you the question

  • SerializesModels stores a snapshot of the model's attributes in the payload
  • Unsaved changes on a model are carried into the queued job
  • A job whose model was deleted is retried until its tries run out
  • Relation constraints applied before dispatch are reapplied on the worker
  • Soft-deleted models cannot be restored inside a queued job