Why your job can't find the model it was dispatched with
Pass an Eloquent model into a queued job and Laravel doesn't serialize the model's data — it serializes a reference to it, and re-fetches the record fresh from the database when the job actually runs. That's a sensible default, but it means "the model as it was when I dispatched this" and "the model as it is when this executes" are two different things, and the gap between them is where a few specific production surprises live.
ON THIS PAGE
How SerializesModels actually works
When a job's constructor is type-hinted with an Eloquent model and the job uses SerializesModels, Laravel doesn't put the model's attributes into the queue payload. It stores the model's class, primary key, and connection name — a lightweight reference, not a snapshot:
class SendOrderConfirmation implements ShouldQueue
{
use SerializesModels;
public function __construct(
public Order $order
) {}
public function handle(): void
{
// $this->order is re-fetched from the database right here,
// not restored from what it looked like at dispatch time
Mail::to($this->order->user->email)->send(new OrderConfirmed($this->order));
}
}
This is why passing models into jobs is safe and cheap even for large models — the payload is tiny. It's also why the model your handle() method sees is whatever's in the database right now, not whatever it was when you called dispatch().
The data you're reading is current, not historical
This is usually what you want — if an order's shipping address changed between dispatch and execution, you generally want the job using the current address. But it's easy to write code that implicitly assumes otherwise:
// Dispatched when an order is placed at $order->total = 49.99
ProcessRefund::dispatch($order);
// ...an hour later, someone edits the order and total changes to 39.99...
// handle() runs now, reads $this->order->total fresh — it's 39.99,
// not the 49.99 the refund was actually supposed to be for
The bug here isn't in Eloquent or the queue — it's in assuming a model reference behaves like a value that was captured at dispatch time. If a job needs to act on data as of the moment it was dispatched, that value needs to be passed explicitly, not read off the model inside handle():
public function __construct(
public Order $order,
public float $refundAmount // captured explicitly at dispatch time
) {}
The record can be gone by the time the job runs
If the underlying row is deleted before the job executes, re-fetching it fails — by default, that's a ModelNotFoundException, and the job fails and follows your normal retry/failed-job path.
Sometimes that's correct: if the work genuinely can't proceed without the record, failing loudly is the right call. But often a missing model just means the work is no longer relevant — the order was cancelled, the post was deleted — and failing the job (with retries, alerts, a failed_jobs row) is noise, not signal. For that case, Laravel gives you an explicit opt-out:
class ProcessOrder implements ShouldQueue
{
use SerializesModels;
public bool $deleteWhenMissingModels = true;
public function __construct(
public Order $order
) {}
}
With this set, a missing model causes the job to be silently deleted from the queue instead of failing — worth doing deliberately per-job, not as a blanket default, since it does mean a genuinely broken reference fails silently instead of surfacing anywhere.
Polymorphic models need a morph map to serialize reliably
If you're passing a polymorphic (morphTo) model into a job, Laravel needs to know how to reconstruct the right class on the other end. Without a registered morph map, it falls back to storing the fully-qualified class name — which breaks if you ever rename or move that model class, and a job sitting in the queue during a deploy won't survive that rename. A morph map, defined once in a service provider, decouples the stored reference from your actual namespace:
Relation::enforceMorphMap([
'post' => Post::class,
'comment' => Comment::class,
]);
Checklist before dispatching a job with a model
- Does anything in
handle()implicitly assume the model looks the way it did at dispatch time? If so, pass that specific value explicitly instead of relying on a fresh read. - If the underlying record could plausibly be deleted before this job runs, is that an expected case (
deleteWhenMissingModels = true) or a real failure you want surfaced? - If you're passing polymorphic models through jobs, do you have a morph map registered so a class rename doesn't break jobs already sitting in the queue?