afterCommit(): Dispatch Queue Jobs Only After the Transaction Commits

📖 2 minutes read

You dispatch a queue job right after saving a model. The job fires, tries to look up the record… and it’s not there. The transaction hasn’t committed yet.

This is one of those bugs that works fine locally (where your database has near-zero latency) but bites you in production with read replicas or under load.

The Problem

Consider this common pattern:

DB::transaction(function () use ($report) {
    $report->status = 'generating';
    $report->save();

    GenerateReport::dispatch($report);
});

The job gets dispatched inside the transaction. Depending on your queue driver, the job might start executing before the transaction commits. The job tries to find the report with status = 'generating', but the database still shows the old state.

The Fix: afterCommit()

Laravel provides afterCommit() on dispatchable jobs to ensure the job only hits the queue after the wrapping transaction commits:

DB::transaction(function () use ($report) {
    $report->status = 'generating';
    $report->save();

    GenerateReport::dispatch($report)->afterCommit();
});

Now the job waits until the transaction successfully commits before being pushed to the queue. If the transaction rolls back, the job never dispatches at all.

Setting It Globally

If you want all your jobs to behave this way by default, add the property to your job class:

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

    public $afterCommit = true;

    // ...
}

Or set it in your queue.php config for the entire connection:

// config/queue.php
'redis' => [
    'driver' => 'redis',
    'connection' => 'default',
    'queue' => 'default',
    'after_commit' => true,  // All jobs wait for commit
],

When You Actually Need This

This pattern is critical when:

  • You use read replicas — the replica might not have the committed data yet
  • Your job uses SerializesModels — it re-fetches the model from the database when deserializing
  • You dispatch jobs that depend on the data you just saved in the same transaction
  • You’re dispatching webhook notifications — the external system might call back before your DB commits

The Gotcha

If there’s no wrapping transaction, afterCommit() dispatches immediately — same as without it. It only delays when there’s an active transaction to wait for.

This is a good thing. It means you can set $afterCommit = true on all your jobs without worrying about jobs that are dispatched outside transactions.

One of those small changes that prevents a whole class of race condition bugs in production.

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 *