skip to content

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?

level: seniorimportance: should knowfreq 32%

answer

  1. named mailers in config/mail.php
  2. MAIL_MAILER defaults to log
  3. transport failover: ordered mailers
  4. roundrobin spreads load
  5. retry_after is 60 seconds

basics

~20 s

Define 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
<?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

for a junior

Recall that config/mail.php defines named mailers, that MAIL_MAILER picks the default, and that the Laravel 13 skeleton defaults to log.

for a middle

Explain transports versus mailers, what the failover and roundrobin transports do with their mailers list, and how Mail::mailer() picks a mailer per message.

for a senior

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.

for a principal

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.