Email is the feature you find out is broken from your users, not your dashboard. Laravel hands the message to a queue, the queue hands it to a provider, the provider hands it to an inbox — and any of those handoffs can fail quietly. "Laravel didn't send it" and "the provider bounced it" look identical from the outside (no email arrived) but have completely different fixes, and you can't tell them apart without watching both the send side and the delivery side. Monitoring mail is mostly about closing that gap.
Always queue mail in production
Synchronous sends block the request for 100-2000ms per email. Queue it.
// Bad — blocks the signup request
Mail::to($user)->send(new WelcomeMail($user));
// Good — dispatches to a queue worker
Mail::to($user)->queue(new WelcomeMail($user));
// Better — delayed so it feels less bot-like
Mail::to($user)->later(now()->addMinutes(2), new WelcomeMail($user));If your mail worker isn't running, queued mail silently piles up. See our queue monitoring guide.
Log every send and failure
Laravel fires events on every mail operation.
use Illuminate\Mail\Events\MessageSent;
use Illuminate\Mail\Events\MessageSending;
use Illuminate\Support\Facades\Event;
public function boot(): void
{
Event::listen(MessageSent::class, function (MessageSent $event) {
\DB::table('mail_log')->insert([
'subject' => $event->message->getSubject(),
'to' => collect($event->message->getTo())->map->getAddress()->implode(', '),
'mailer' => $event->data['__laravel_notification_mailer'] ?? 'default',
'sent_at' => now(),
]);
});
}Mail provider webhooks fill in what happens after the send — delivered, bounced, complained, opened. Register a webhook endpoint with your provider and log those too.
Track three metrics
SEND RATE
Mails sent per minute by class. A sudden drop usually means the queue worker died or SMTP credentials expired.
SEND DURATION
p95 time from dispatch to SMTP accept. Spikes usually mean provider rate limits or network issues.
DELIVERY RATE
Delivered / attempted from provider webhooks. A drop usually means IP reputation issues or bad recipient lists.
Where it actually breaks
Because email crosses three systems, the useful skill is knowing which one swallowed the message — and the split is clean once you instrument both sides.
If Laravel's MessageSent event never fired, the failure is yours: a dead queue worker, a Mailable that threw while rendering its Blade view, or MAIL_MAILER=log quietly left on in production. The fix is in your code or your workers.
If MessageSent fired but the provider's webhook reports a bounce, a complaint, or no delivery, the failure is downstream: a bad recipient address, IP reputation, a spam filter. The fix is list hygiene or provider configuration, not your application.
Watch only one side and you're guessing. The send-side events tell you the app did its job; the provider webhooks tell you whether the message actually landed. You need both to answer the question every "my email didn't arrive" ticket is really asking: whose problem is it?
What to alert on
- Send rate drops to 0 for >5 minutes — worker down or credentials bad
- Spike in MessageFailed — 20+ failures of the same Mailable class in 10 min
- Bounce rate > 2% — list hygiene problem, risking IP reputation
- Complaint rate > 0.1% — users marking as spam, provider will soon throttle you
Route mail alerts to Slack or Discord, never email — if mail is broken, email alerts are exactly what you can't receive.
THE EASY WAY
NightOwl logs every mail send with full context
NightOwl's mail watcher records every send — Mailable class, to/from, subject, mailer, duration, and any thrown exception — tied to the request or queued job that triggered it. Grouped per Mailable class so you can see p95 send duration and failure rate over any time range.
composer require nightowl/agent
php artisan nightowl:install