skip to content

In Laravel, how do Mail::to()->send(), queue() and later() differ, and what changes when a mailable implements ShouldQueue?

level: middleimportance: must knowfreq 58%

answer

  1. request waits for the transport
  2. SendQueuedMailable job for a worker
  3. later(): DateTime, DateInterval or seconds
  4. ShouldQueue turns send() into queue()
  5. sendNow() forces immediate delivery

basics

~10 s

send() delivers during the request unless the mailable implements ShouldQueue; queue() pushes a SendQueuedMailable job for a worker; later() queues it with a delay. With ShouldQueue even send() queues, and sendNow() forces synchronous delivery.

solid answer

~40 s

`Mail::to($customer)->send($mail)` renders the message and hands it to the transport inside the current request, so a slow SMTP server or mail API slows the response. `queue($mail)` wraps the mailable in an `Illuminate\Mail\SendQueuedMailable` job and pushes it onto the queue; a worker renders and sends it later. `later($delay, $mail)` does the same with a delay given as a `DateTimeInterface`, a `DateInterval` or seconds. Implementing `ShouldQueue` on the mailable makes a plain `send()` queue too, so callers cannot forget, while `sendNow()` bypasses that. Because the mailable is serialized, `SerializesModels` stores model identifiers and re-queries them in the worker. You pick the queue with `onQueue()`/`onConnection()` or, in Laravel 13, `#[Queue]`/`#[Connection]` attributes; mail queued inside a database transaction should use `afterCommit()`.

code

php · 20 lines
php
<?php

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

// Rendered and handed to the transport before the response
Mail::to($order->customer)->send(new OrderConfirmed($order));

// Pushed as a SendQueuedMailable job; a worker sends it
Mail::to($order->customer)->queue(new OrderConfirmed($order));

// Pushed with a ten-minute delay
Mail::to($order->customer)->later(now()->addMinutes(10), new OrderConfirmed($order));

// Pushed only after the surrounding transaction commits
DB::transaction(function () use ($order) {
    $order->update(['status' => 'paid']);
    Mail::to($order->customer)->queue((new OrderConfirmed($order))->afterCommit());
});

go deeper

for a junior

Recall the three verbs: send() now, queue() in the background, later() with a delay. Know that a queue worker must be running for queued mail to go out.

for a middle

Explain the SendQueuedMailable job, what SerializesModels stores, how ShouldQueue redirects send(), and how sendNow(), onQueue() and the Laravel 13 queue attributes fit in.

for a senior

Show the transaction race: after_commit is false in the skeleton, so mail depending on fresh rows needs afterCommit() or ShouldQueueAfterCommit, and a failed() hook to recover lost confirmations.

for a principal

Argue for a team rule such as every customer-facing mailable implements ShouldQueue, and weigh the cost: a worker to operate and monitor versus request latency tied to a mail provider.

## Three ways to hand off a mailable Laravel's `Mail` facade returns a pending mail from `Mail::to(...)`, and that object offers the delivery verbs. They differ only in *where* and *when* the message is rendered and passed to the mail transport. | Call | Where it runs | What the caller gets | |---|---|---| | `send($mailable)` | in the current PHP process, before the response | a `SentMessage` (or `null`) — unless the mailable is `ShouldQueue` | | `queue($mailable)` | a queue worker, as soon as it picks the job up | the queue's push result | | `later($delay, $mailable)` | a queue worker, after the delay | the queue's push result | | `sendNow($mailable)` | the current process, always | a `SentMessage` (or `null`) | Order confirmations are the classic case. Rendering a template and talking to an SMTP server or an HTTP mail API can take hundreds of milliseconds — longer when the provider is slow — and none of that needs to happen before the customer sees "thank you for your order". ## What queueing actually does 1. `queue()` wraps your mailable in an `Illuminate\Mail\SendQueuedMailable` job. 2. The job is **serialized** into the queue payload. The generated mailable uses `SerializesModels`, so an `Order` property is stored as an identifier (class, key, connection, loaded relation names), not as a snapshot of its attributes. 3. A worker pulls the job, unserializes it — re-querying the order from the database — and calls the mailable's `send()`. 4. Rendering, `attachments()` and the transport call all happen in the worker. A worker must be running on the connection you push to. In a new Laravel 13 app `QUEUE_CONNECTION` is `database`; if you set it to `sync`, "queued" mail is sent immediately in the same process and gains nothing. ## ShouldQueue on the class If every send of a mailable should be backgrounded, implement `Illuminate\Contracts\Queue\ShouldQueue`: ```php class OrderConfirmed extends Mailable implements ShouldQueue ``` The mailer checks for the interface: `send()` on a `ShouldQueue` mailable is redirected to `queue()`. That protects you from a teammate calling `send()` in a controller. When you genuinely need synchronous delivery — a console command, a health check — `Mail::to($user)->sendNow($mail)` skips the check. ## Choosing the connection and queue - Fluent: `(new OrderConfirmed($order))->onConnection('redis')->onQueue('mail')`, available because the class uses `Queueable`. - Attributes: `#[Connection('redis')]` and `#[Queue('mail')]` from `Illuminate\Queue\Attributes` on the class. These are part of Laravel 13's attribute push; the fix that makes a mailable's `queue()` and `later()` honour them landed in 13.13.0, so on an older 13.x use the fluent calls. - `Mail::to($u)->later(now()->addMinutes(10), $mail)` delays one send; a `#[Delay]` attribute on a queued mailable sets a default delay. ## Mail inside a database transaction Queue connections in the Laravel 13 skeleton set `after_commit` to `false`. If you queue a mailable inside `DB::transaction()`, a fast worker can run the job **before the transaction commits**; `SerializesModels` then re-queries an order that does not exist yet (or is stale) and the job fails. Two fixes: - call `->afterCommit()` on the mailable (in the constructor or at the call site), or - implement `Illuminate\Contracts\Queue\ShouldQueueAfterCommit` instead of `ShouldQueue`. Either way the job is pushed only once the open transactions commit; if the transaction rolls back, nothing is pushed. ## Picking the right verb - **Customer-facing mail triggered by a web request** — order confirmations, shipping notices: queue it, ideally by making the mailable `ShouldQueue` so the choice lives with the class. - **A follow-up that should arrive later** — "how was your order?" a few hours after delivery: `later()` with a `DateTimeInterface` works, but a delayed job sits in the queue the whole time. For delays of days, a scheduled command that finds due orders is easier to inspect and cancel. - **A console command or an internal alert** where nothing waits on the response: `send()` or `sendNow()` is fine, and failures surface immediately in the command's output. - **Anything in a loop over many recipients**: queue each message, and build a new mailable per recipient. One subtlety of queued mail: the message is rendered in the worker, so it reflects the order *as it is when the worker runs*, not as it was when you queued it. That is usually what you want — but it is another reason to make sure the data exists by then. ## When a queued mail fails A queued mailable can define `failed(Throwable $e)`, which Laravel calls when the job finally fails — the place to flag the order for a manual resend. How many attempts it gets and how long to wait between them belong to the queue configuration, not to the mail API. ## Summary - `send()` is synchronous unless the class is `ShouldQueue`; `sendNow()` is always synchronous. - `queue()` and `later()` push a `SendQueuedMailable` job; models travel as identifiers. - Use `afterCommit()` or `ShouldQueueAfterCommit` when the mail depends on rows written in the same transaction.

  • Why does a queued order-confirmation mail sometimes fail with a missing Order when it is queued inside DB::transaction()?
    The job is pushed immediately, and with `after_commit` set to `false` (the skeleton's value) a worker can unserialize it before the transaction commits; `SerializesModels` re-queries the order and does not find it. Call `->afterCommit()` on the mailable or implement `ShouldQueueAfterCommit`, so the job is pushed only after commit and never on rollback.
  • How do you send a ShouldQueue mailable synchronously, for example from an Artisan command?
    Use `Mail::to($user)->sendNow($mail)`. `sendNow()` calls the mailable's `send()` directly, skipping the `ShouldQueue` check that makes a plain `send()` push a job.
  • Does queueing help if QUEUE_CONNECTION is set to sync?
    No. The `sync` connection runs a pushed job immediately in the same process, so the request still waits for rendering and the transport. Queueing only moves the work off the request with an asynchronous connection such as `database` or `redis` and a running worker.

send() is standing at the post office counter until the clerk has taken your letter; queue() is dropping it in the outbox for the courier's next round, and later() is a letter marked 'do not deliver before Friday'. A ShouldQueue mailable is a letter that always goes in the outbox, even if you walk it to the counter.

saying these in an interview costs you the question

  • send() never queues, whatever interfaces the mailable implements.
  • queue() sends the email from a background thread of the same request.
  • SerializesModels stores every attribute of the order in the job payload.
  • later() relies on the task scheduler rather than a queue worker.
  • Mail queued inside a transaction always waits for the commit by default.