Read Replica Lag Breaks Laravel Queue Jobs Before handle() Runs

📖 2 minutes read

You dispatch a queue job right after saving a model. The job picks it up in milliseconds. And then — ModelNotFoundException.

The model definitely exists. You just created it. You can query it manually. But the queue worker says otherwise.

The Culprit: Read Replicas

If your database uses read replicas (and most production setups do), there’s a lag between the primary and the replicas. Usually milliseconds, sometimes longer under load.

Laravel’s SerializesModels trait only stores the model’s ID when the job is serialized. When the worker deserializes it, it runs a fresh query — against the read replica. If the replica hasn’t caught up yet, the model doesn’t exist from the worker’s perspective.

The cruel part: this happens before your handle() method runs. Your retry logic never fires because the job fails during deserialization.

The Fix: afterCommit()

Laravel has a built-in solution. Add afterCommit to your job:

class GenerateInvoice implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public $afterCommit = true;

    public function handle(): void
    {
        // This now runs after the transaction commits
        // and the replica has had time to sync
    }
}

Or dispatch with the method:

GenerateInvoice::dispatch($invoice)->afterCommit();

Other Options

If afterCommit isn’t enough (replicas can still lag after commit), you have two more tools:

// Option 1: Add a small delay
GenerateInvoice::dispatch($invoice)->delay(now()->addSeconds(5));

// Option 2: Skip missing models instead of failing
public $deleteWhenMissingModels = true;

Option 2 is a silent skip — the job just disappears. Use it only when the job is truly optional (like sending a notification for a model that might get deleted).

The Lesson

If you’re dispatching queue jobs immediately after writes and seeing phantom ModelNotFoundException errors, check your database topology. Read replicas + SerializesModels + fast workers = a race condition that only shows up under load. afterCommit() is the cleanest fix.

Daryle De Silva

VP of Technology

11+ years building and scaling web applications. Writing about what I learn in the trenches.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *