0% прочитано

Laravel DB::afterCommit() and Queue Jobs: Why a Job Should Not Run Before the Commit

In ICanUp, I run IndexNow notifications and related queue operations only after a successful database commit. Using real Laravel code, I show why dispatching work inside a transaction can expose uncommitted state or survive a rollback, and how DB::afterCommit() creates a clear boundary between database state and external side effects.

8 вересня 2026 р. 9 хв читанняLaravel

While working on IndexNow in ICanUp, I needed to run notifications and related asynchronous operations after changing Posts, Categories, and other models.

The database state itself changes inside DB::transaction(). But an external side effect should not start just because the code inside the transaction has already executed save().

I needed a stronger guarantee - run the next operation only after the transaction has actually committed successfully.

My short rule is simple: if a job, notification, or external API call depends on data currently being changed inside a transaction, I do not run it before the commit. In Laravel, I use DB::afterCommit() for that boundary.
Laravel DB afterCommit transaction and queue job flow
The database transaction must complete successfully before queue jobs and external side effects are allowed to run.

The Real ICanUp Case

Post creation in ICanUp already has a clear transaction boundary.

  • the service saves the Post

  • synchronizes translations and series membership

  • resolves IndexNow URLs

  • and tracks media changes

Operations that go beyond the database transaction are delayed until the commit.

PHP - PostService transaction and afterCommit
DB::transaction(function () use ($request): void {    $post = new Post;    $post->author_id = (int) auth()->id();     $post = $this->savePost($post, $request);    $post->syncTranslations($request->validated());    $this->syncSeriesMembership($post, $request);     $indexNowUrls = $this->indexNowUrlService        ->postTargets($post);     $changedMediaIds = $this->mediaUsageService        ->syncSingleForContext(            $post,            null,            $post->image_media_id,            MediaUsageContextEnum::IMAGE,        );     DB::afterCommit(function () use (        $post,        $changedMediaIds,        $indexNowUrls,    ): void {        $this->dispatcher->dispatch($changedMediaIds);        $this->indexNowNotificationService            ->notify($indexNowUrls);        $this->indexNowPublicationScheduler            ->schedulePost($post);    });});

The important part for me is not the DB::afterCommit() syntax itself. It is the responsibility boundary: database state becomes durable first, and only then can other parts of the system react to it.

What Can Go Wrong When You Dispatch Inside a Transaction

At first, it seems reasonable to call a notification service or dispatch a job immediately after save.

The problem is that executing PHP code inside a transaction does not mean another process can already see those changes.

This becomes especially visible with a queue worker that runs independently from the HTTP request.

SituationWhat can happenWhy it is a problem
Worker starts very quicklyThe job reads old state or cannot find the new recordThe transaction has not committed yet
Transaction rolls backThe job was already dispatchedAsync work reacts to state that does not exist in the database
External API already startedThe API call cannot be rolled back with the databaseDatabase state and the external system diverge

The Worker Can Be Faster Than the Commit

A queue worker does not have to wait for the current HTTP request to finish. Once a job reaches the queue, a worker may pick it up while the database transaction is still open.

PHP - Potentially unsafe dispatch
DB::transaction(function (): void {    $post = Post::query()->create([        // ...    ]);     ProcessPost::dispatch($post->id);});

Depending on the connection, isolation level, and what the job reads, the result can be a missing record, an older value, or incomplete state.

A Rollback Cannot Undo an External Side Effect

The more important case is a transaction that performs several operations and then fails near the end.

The database can roll back. But if a notification, webhook, or external API call has already started, the database rollback cannot automatically cancel that external action.

A database transaction controls database state. It cannot roll back an email, HTTP request, IndexNow submission, or a job that another worker has already started processing.

What DB::afterCommit() Changes

DB::afterCommit() lets me register a callback during the transaction but execute it only after a successful commit.

До
php
DB::transaction(function (): void {    $post->save();     $this->indexNowNotificationService        ->notify($urls);});
Після
php
DB::transaction(function (): void {    $post->save();     DB::afterCommit(function () use ($urls): void {        $this->indexNowNotificationService            ->notify($urls);    });});

In the second version, the external reaction is no longer part of the optimistic execution path of the transaction.

Laravel must complete the database transaction successfully first. Only then does the callback become relevant.

If the transaction does not commit, the afterCommit callback should not trigger a side effect for state that was never persisted.

ICanUp Runs Several Reactions After the Same Commit

PHP - Side effects after commit
DB::afterCommit(function () use (    $post,    $changedMediaIds,    $indexNowUrls,): void {    $this->dispatcher->dispatch($changedMediaIds);     $this->indexNowNotificationService        ->notify($indexNowUrls);     $this->indexNowPublicationScheduler        ->schedulePost($post);});
  • Media usage changes can notify other parts of the system.
  • The IndexNow notification can prepare submission for the current public URLs.
  • The publication scheduler can schedule a future SEO operation.
  • All of these reactions receive committed database state.

This is especially useful when one transaction changes not only the main model but also translations, relationships, or media references.

The Same Principle Applies to Content Blocks

Content blocks in ICanUp are also synchronized transactionally.

Several media usages may change during one sync. The related events are dispatched only after the commit.

PHP - Content block media events after commit
DB::afterCommit(static function () use (    $changedMediaIds): void {    foreach (        array_unique($changedMediaIds) as $mediaId    ) {        event(            new MediaUsageChangedEvent(                (int) $mediaId            )        );    }});

For me, this is a useful sign that afterCommit should not be treated as a random local fix. It is a system pattern for moving from transactional state to asynchronous or external reactions.

DB::afterCommit() and Queue after_commit Are Related but Different

Laravel also has queue-level mechanisms for dispatching work after a commit.

DB::afterCommit() is broader than a queue job. Inside the callback I can trigger a notification service, event dispatcher, scheduler, or another operation.

That is why I use it when I want the transaction boundary to be explicit in the application service.

MechanismLevelWhen it is useful
DB::afterCommit()Database transaction callbackWhen several different operations should run after commit
Queue after commitQueue dispatch behaviorWhen the main concern is the job dispatch moment
Direct dispatchImmediate operationWhen the work does not depend on transactional state

Not Every Job Needs afterCommit

I do not wrap every dispatch in afterCommit automatically.

If an operation does not depend on data from the current transaction, or it already runs outside a transaction, an additional boundary may add no value.

The important part is not the helper itself. It is the causal relationship between committed state and the next action.

My practical question is simple: should this operation be allowed to happen if the database transaction rolls back a second later? If the answer is "no", it is a strong candidate for afterCommit.

What I Verify in Tests

For this type of code, I care about more than whether a service method was eventually called. I also want the transaction semantics to remain correct.

  • The side effect does not run before a successful commit.
  • After commit, the operation uses the current state.
  • On rollback, an external reaction should not behave as if the change was persisted.
  • An update flow preserves both old and new URLs when an external service needs both.
  • Duplicate URLs are removed before dispatch.

Why This Matters for IndexNow

IndexNow is an external indexing signal. Once a URL has been submitted, the search engine knows nothing about my database transaction.

Sending a URL before a successful commit would therefore be conceptually wrong: I could announce state that is later rolled back.

AfterCommit lets me place the external SEO side effect after the database source of truth.

The database becomes the truth first. Only then should other systems be told about that truth.

What I Kept for Next Time

  • Do not dispatch a job only because a model has already executed save inside a transaction.
  • Separate transactional database changes from external side effects.
  • Remember that queue workers can start processing faster than the request finishes.
  • Do not expect a database rollback to cancel an HTTP request or notification that already happened.
  • Use DB::afterCommit() when an application service has a clear post-commit phase.
  • Test the rollback path separately from the happy path.

Conclusion

DB::afterCommit() turned out to be more than a convenient Laravel helper for me.

It creates a clear boundary between changes that can still be rolled back and operations that already leave the database.

In ICanUp, that is especially important for IndexNow, media events, and publication scheduling. The transaction becomes committed state first. Only then do I run the code that reacts to that state.

With this model, queue jobs and external integrations become much more predictable, and a rollback is less likely to leave side effects for changes that never actually happened.