A notification in Laravel can fan out to five channels from one dispatch — email, Slack, SMS, a database row, a broadcast — and each of those can succeed or fail independently. That's what tracking has to account for: "the OrderShipped notification went out" isn't a single yes/no, it's five separate deliveries with five different failure modes. A user can get the email and not the SMS, or the database row and nothing else. Monitoring notifications means watching delivery per channel, not per notification.
The three notification events
Every notification dispatch fires three events you can listen to per channel:
app/Providers/EventServiceProvider.php
use Illuminate\Notifications\Events\NotificationSending;
use Illuminate\Notifications\Events\NotificationSent;
use Illuminate\Notifications\Events\NotificationFailed;
use Illuminate\Support\Facades\Event;
public function boot(): void
{
Event::listen(function (NotificationSent $event) {
logger()->info('Notification sent', [
'class' => get_class($event->notification),
'channel' => $event->channel,
'notifiable_type' => get_class($event->notifiable),
'notifiable_id' => $event->notifiable->getKey(),
]);
});
Event::listen(function (NotificationFailed $event) {
logger()->error('Notification failed', [
'class' => get_class($event->notification),
'channel' => $event->channel,
'data' => $event->data,
]);
});
}Queue every notification — then track the queue
Every notification that hits an external service (mail, Slack, SMS) should implement ShouldQueue so it doesn't block the request:
app/Notifications/OrderShipped.php
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;
class OrderShipped extends Notification implements ShouldQueue
{
use Queueable;
public function via($notifiable): array
{
return ['mail', 'database'];
}
public function shouldSend($notifiable, $channel): bool
{
// Rate-limit — don't send twice within an hour
$key = "shipped:{$notifiable->id}:{$channel}";
if (cache()->has($key)) return false;
cache()->put($key, true, now()->addHour());
return true;
}
}Queued notifications fail as queued jobs. Pair notification tracking with failed-job monitoring to catch silent delivery failures.
Metrics that matter
- Delivery rate per channel — sent / (sent + failed). Drops indicate provider issues.
- Send count per notification class — catches runaway sends during incidents.
- Queue lag — time between dispatch and actual delivery. Rising lag = workers overwhelmed.
- Failure clustering by recipient — same user failing across multiple notifications usually means a stale email address or broken webhook.
Gotchas by channel
- Mail: A 200 from your SMTP doesn't mean the email was delivered to the user. Track bounces via your mail provider's webhook and mark users as undeliverable in your database.
- Slack webhooks: Silent failures are common if a workspace admin rotated the URL. Fall back to storing the last 20 failures and page someone if all recent sends fail.
- SMS: Carriers drop small percentages — don't alert on single failures, alert on rate drops.
- Database: 100% reliable, but don't use it as your primary channel — users won't see it unless they log in.
- Broadcast: Depends on your broadcaster (Pusher, Ably, Reverb). WebSocket disconnects mean delivery isn't guaranteed for offline users.
THE EASY WAY
NightOwl tracks every notification with per-channel delivery rate
The notifications dashboard groups every dispatch by notification class + channel with delivery rate, failure count, and p95 send duration. Click into a class to see every send, recipient, and status. Alerts fire on delivery-rate drops across any configured channel.
composer require nightowl/agent
php artisan nightowl:installFrom $5/month flat. Data in your PostgreSQL.