Currently Available: Need a skilled Software Developer for your next project?
Categories
Laravel

Queueing Laravel Mail Safely with After-Commit Transactions

When a Laravel request saves a record and sends an email about it, the order of those actions matters. A database transaction groups database changes so Laravel can save them together or roll them back. A mailable is a Laravel class that describes an email. If a request queues a mailable before its transaction commits, a worker can start the email job before the database makes the new data available.

If an email depends on changes made in a transaction, queue the mailable and wait to dispatch it until the transaction commits. Laravel’s afterCommit() option does this: it releases the queued job after a successful commit and discards it if the transaction rolls back. The option coordinates database changes with queue dispatch; it does not guarantee that the recipient receives the email.

How Laravel sends queued mail

A request can send an ordinary mailable immediately with Mail::to(...)->send(...). Laravel calls the configured mailer in the current PHP process, making the request wait for the mail operation. The request path can handle a mail transport error, and sending adds to request time. If the email goes out and the database transaction later rolls back, Laravel cannot retract the message.

A request can instead call Mail::to(...)->queue(...). Laravel places a job on a configured queue, and a queue worker, a separate process that takes jobs from the queue and runs them, sends the email later. The request returns without waiting for the worker to send the message; delivery time still depends on worker availability and the work ahead in the queue. Laravel describes queued mail as a way to move email work out of the request path.

The mailable’s contract also matters: a mailable that implements ShouldQueue is queued even when the caller uses send(). Check both the method call and the mailable class when determining whether the request will wait for mail work. Also check the queue driver: Laravel’s sync driver runs jobs immediately in the current PHP process, so it does not provide background delivery. Background sending requires an asynchronous queue backend and a running worker, as described in Laravel’s queue documentation.

Why mail should wait for the transaction

Suppose a request creates an order and queues an order-confirmation email inside the same transaction. If a worker picks up the job before the transaction commits, the worker may not see the order or related changes. The job may fail, or the message may use incomplete data. If the transaction then rolls back, a message that already went out could describe an order that was never saved.

Laravel’s database transaction helper commits when its closure succeeds and rolls back when an exception escapes the closure. The transaction documentation describes that behavior. Deferring queued mail until commit prevents the worker from racing ahead of the transaction and prevents Laravel from dispatching mail deferred from a transaction that rolls back.

Use this behavior when the email depends on data created or changed in the transaction, such as an order confirmation, receipt, or invitation. The worker waits until Laravel commits the database changes, so it does not try to use data that the transaction has not saved yet.

Defer a mailable until commit

For a mailable that should always wait for its transaction to commit, implement ShouldQueue and call afterCommit() in its constructor:

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;

class OrderConfirmed extends Mailable implements ShouldQueue
{
    use Queueable, SerializesModels;

    public function __construct(public Order $order)
    {
        $this->afterCommit();
    }
}

Then queue it while creating the order:

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;

DB::transaction(function () use ($orderData, $user) {
    $order = Order::create($orderData);

    Mail::to($user)->queue(new OrderConfirmed($order));
});

Laravel holds the queued job until the transaction commits. If the transaction rolls back, Laravel discards the deferred job. The mailable’s constructor makes the rule apply wherever that class is queued. Laravel also supports setting afterCommit() at the call site when the transaction-dependent behavior should be visible where the mailable is queued.

For broader coverage, set after_commit to true on a queue connection. Laravel then defers jobs dispatched during open transactions on that connection, including queued mail; the setting also applies to other queued work such as event listeners and notifications. Choose a per-mailable setting when only specific emails depend on transaction state. Choose the connection setting when all jobs on that connection should wait for open transactions to commit. When no transaction is open, Laravel dispatches the job immediately. See the queue connection options for configuration details.

What after-commit does not guarantee

After-commit controls when Laravel dispatches a job. It does not mean the worker sends the email at commit time, and it does not prove the recipient’s inbox received the message. Queue delays, worker failures, and mail transport problems remain possible. Keep workers running and inspect failed jobs using Laravel’s queue failure and retry tools.

Retries also mean a mail job should not be treated as an exactly-once delivery guarantee. A job may be processed more than once under some queue configurations, so consider whether repeated processing could produce duplicate email. Laravel’s queue guidance on retries describes worker and retry settings that operators need to coordinate.

When a queued mailable contains a Laravel Eloquent model, the PHP object Laravel uses to represent a database record, choose whether the email should show the model’s values when the worker runs or values copied from the transaction. Laravel serializes queued models and retrieves them again when the worker processes the job. The model may have changed by then, or it may have been deleted. If the message must show the values as they were during the transaction, pass a copy of those values to the mailable. Otherwise, the worker may use the model’s later values.

Finally, the database and the mail provider remain separate systems. afterCommit() prevents premature dispatch and suppresses dispatch for a rolled-back transaction, but it does not make the database commit and external email delivery one atomic action. If you need stronger guarantees across that boundary, evaluate a durable publication design separately. For example, an outbox stores messages in the database so another process can publish them later.

What I'm building

Delegate tasks. Get software.

Give Vroni a GitHub issue, bug report, spec, or rough idea. It reads the repo, plans the change, writes code, runs checks, and works toward a review-ready pull request.

Take a look at vroni.com

Email updates

Usually a new article and a few links I found interesting.

No spam. Unsubscribe with one click.

Leave a Reply

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