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.