Your Laravel app's only mail provider had an outage and order confirmations were lost; how would you configure its mailers, including failover and roundrobin, to reduce that risk?
answer
- named mailers in config/mail.php
- MAIL_MAILER defaults to log
- transport failover: ordered mailers
- roundrobin spreads load
- retry_after is 60 seconds
basics
~20 sDefine each provider as a named mailer in config/mail.php, then make the default a failover mailer listing them in priority order, so a transport error moves that message to the next one. Queue the mail too, so a total outage leaves retryable jobs.
solid answer
~40 s`config/mail.php` holds named **mailers**, each with a `transport` — `smtp`, `ses`, `postmark`, `resend`, `sendmail`, `log`, `array` and others — and `MAIL_MAILER` picks the default, which is `log` in the Laravel 13 skeleton and only writes messages to the log. For resilience, define a mailer with `'transport' => 'failover'` and `'mailers' => ['postmark', 'ses']`, and set `MAIL_MAILER=failover`: when sending through the first mailer throws, the same message is tried on the next, and the failed mailer is skipped for `retry_after` seconds (60 in the shipped config). `roundrobin` instead starts from a random mailer and rotates, spreading volume rather than preferring one provider. Failover only covers errors at hand-off; a message the provider accepted and later bounced is not retried. So also queue the confirmations, and use `Mail::mailer('postmark')` where one message must use a specific provider.
code
php · 17 lines<?php
// config/mail.php (excerpt)
return [
'default' => env('MAIL_MAILER', 'log'),
'mailers' => [
'postmark' => ['transport' => 'postmark'],
'ses' => ['transport' => 'ses'],
'failover' => [
'transport' => 'failover',
'mailers' => ['postmark', 'ses'],
'retry_after' => 60,
],
],
];go deeper
Recall that config/mail.php defines named mailers, that MAIL_MAILER picks the default, and that the Laravel 13 skeleton defaults to log.
Explain transports versus mailers, what the failover and roundrobin transports do with their mailers list, and how Mail::mailer() picks a mailer per message.
Show what failover cannot cover — accepted-then-bounced mail and total outages — and pair it with queued mail, a failed() hook and a production default that is never log.
Weigh a second provider's cost, domain setup and reputation split against the risk of lost transactional mail, and decide which message streams deserve dedicated mailers.
## Mailers and transports Laravel separates two ideas in `config/mail.php`: - A **transport** is the delivery mechanism: `smtp`, `sendmail`, `ses`, `ses-v2`, `postmark`, `resend`, `mailgun`, `log`, `array`, plus the two composite transports `failover` and `roundrobin`. - A **mailer** is a named configuration that uses one transport with its settings. The `default` key, read from `MAIL_MAILER`, chooses which mailer an ordinary `Mail::to(...)->send(...)` uses. API transports need their SDK or bridge installed — for example `symfony/postmark-mailer` with `symfony/http-client` for Postmark, `resend/resend-php` for Resend, `aws/aws-sdk-php` for SES — and read credentials from `config/services.php` (`POSTMARK_API_KEY`, `RESEND_API_KEY`, the `AWS_*` variables). ## What the Laravel 13 skeleton ships | Mailer | Transport | Intended use | |---|---|---| | `smtp` | `smtp` | any SMTP relay; defaults `MAIL_HOST=127.0.0.1`, `MAIL_PORT=2525` | | `ses`, `postmark`, `resend` | same-named API transports | production providers | | `sendmail` | `sendmail` | a local MTA binary | | `log` | `log` | development: writes each message to the log (`MAIL_LOG_CHANNEL` picks the channel) | | `array` | `array` | tests: keeps messages in memory | | `failover` | `failover` | example: `smtp`, then `log` | | `roundrobin` | `roundrobin` | example: `ses` and `postmark` | The skeleton's `.env.example` sets `MAIL_MAILER=log`. That is safe for development and a common production surprise: everything "sends" without error, and nothing arrives. ## Failover for availability 1. Define real mailers for two providers, say `postmark` and `ses`, each with working credentials. 2. Define a composite mailer with `'transport' => 'failover'`, `'mailers' => ['postmark', 'ses']` and `'retry_after' => 60`. 3. Set `MAIL_MAILER=failover`. Laravel builds Symfony Mailer's failover transport from the listed mailers. Each message goes to the first healthy mailer; if that call throws a transport error — connection refused, an API 5xx, bad credentials — the same message is immediately tried on the next. A mailer that failed is treated as unavailable for `retry_after` seconds (Laravel passes 60 when the key is missing) before being tried again. All of this happens inside one send call, in whatever process is sending — the request or a queue worker. ## Round robin for load `roundrobin` takes the same shape but a different goal: it starts with a random mailer and moves to the next one for each subsequent message, spreading volume across providers. The docs frame it plainly — failover is about high availability, round robin about load balancing. Use round robin when you deliberately split traffic, for instance to stay inside per-provider sending quotas. ## What neither protects against - **Accepted, then lost.** Once a provider accepts a message, a later bounce, block or spam-folder placement is invisible to the transport; no failover happens. - **Every provider down.** The send still throws. If it ran in the request, the confirmation is gone unless you catch and record it. - **A log fallback.** A failover mailer listing `log` last, as the skeleton's example does, will "succeed" in production by writing to the log. Keep `log` out of production chains. That is why failover pairs with **queued mail**: a queued `SendQueuedMailable` that throws is retried by the queue with backoff, and a `failed()` hook on the mailable can flag the order, so an outage becomes delay rather than loss. ## Choosing a mailer per message `Mail::mailer('postmark')->to($customer)->send($mail)` sends that one message through a named mailer without changing the default. Typical uses: transactional mail on a dedicated provider or stream, or marketing mail on a separate one so its reputation cannot hurt receipts. ## Per-environment defaults The same config file serves every environment; only `MAIL_MAILER` and the credentials change: - **Local:** `log` needs nothing, but you only see raw messages in the log. Sail's default services include Mailpit, and when it is selected Sail rewrites `.env` to `MAIL_MAILER=smtp`, `MAIL_HOST=mailpit`, `MAIL_PORT=1025`, giving a web inbox that catches everything. - **Tests:** the skeleton's `phpunit.xml` sets `MAIL_MAILER=array`, so nothing leaves the process. - **Staging:** a real provider, but consider a global "to" address (`Mail::alwaysTo()`) so staging never emails real customers. - **Production:** the failover mailer, with every mailer in its chain able to deliver for real. A quick production check after deploy — sending one message through each mailer in the chain — catches expired credentials before an outage forces the failover to use them. ## Summary - Name each provider as a mailer; point `MAIL_MAILER` at a real one, not the skeleton's `log`. - `failover` tries mailers in order per message; `roundrobin` rotates for load; both honour `retry_after`. - Queue important mail so a full outage is retried, not lost.
- A new Laravel 13 app raises no errors when sending order mail, yet nothing arrives. What is the likely cause?`MAIL_MAILER` is still `log`, the skeleton default, so each message is written to the log channel instead of being delivered. Set it to a real mailer such as `postmark` or your failover mailer; the log shows the messages that were never sent.
- How is the failover transport different from a queued mail job being retried?Failover acts inside a single send: the next mailer is tried immediately for the same message. A queue retry re-runs the whole job later, after backoff, using the configured mailer again. They compose: a queued mailable sent through a failover mailer first tries both providers, and only if both fail does the job retry.
- What is the array transport for?It stores messages in memory instead of sending them, which suits tests; the skeleton's `phpunit.xml` sets `MAIL_MAILER=array`. Nothing leaves the process and nothing is written to the log.
saying these in an interview costs you the question
- A new Laravel 13 app delivers mail over SMTP by default.
- Failover resends a message that the provider accepted but later bounced.
- Failover needs a queue worker to move a message to the next mailer.
- Mail::mailer('ses') switches the default mailer for the rest of the request.
- Listing log as the last failover mailer is a safe production fallback.