Tech Verse Logo
Enable dark mode
Advanced Laravel Notifications: Queues and Channels

Advanced Laravel Notifications: Queues and Channels

Md. Mostafijur RahmanMMd. Mostafijur Rahman

Md. Mostafijur Rahman

5 min read

Default notification tutorials show you how to send a mail message when a user buys a product. That works fine for small side projects, but production applications break quickly under those assumptions. Users get spammed across four channels at once, queue workers choke on uncommitted database transactions, and custom SMS gateways fail without retry policies.

Managing Per-User Notification Preferences

Don't hardcode channels in your notification's via() method. Users want granular control over whether an alert arrives via email, database, or SMS. Storing these settings in a JSON column on your users table keeps queries simple and avoids extra table joins.

In Laravel 12 running on PHP 8.3, typed properties and dynamic channel resolution make evaluating these preferences clean. Here is how you can dynamically resolve channels inside a notification class based on a user's settings cast.

namespace App\Notifications;

use App\Channels\SmsChannel;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;

class OrderShipped extends Notification implements ShouldQueue
{
    use Queueable;

    public function __construct(public readonly string $orderId) {}

    public function via(User $notifiable): array
    {
        $prefs = $notifiable->notification_preferences ?? [];
        $channels = [];

        if ($prefs['email_order_shipped'] ?? true) {
            $channels[] = 'mail';
        }

        if ($prefs['database_order_shipped'] ?? true) {
            $channels[] = 'database';
        }

        if (($prefs['sms_order_shipped'] ?? false) && $notifiable->phone_number) {
            $channels[] = SmsChannel::class;
        }

        if ($prefs['broadcast_order_shipped'] ?? true) {
            $channels[] = 'broadcast';
        }

        return $channels;
    }

    public function toMail(User $notifiable): MailMessage
    {
        return (new MailMessage)
            ->subject("Order #{$this->orderId} Shipped")
            ->line("Your package is on its way.")
            ->action('View Order', url("/orders/{$this->orderId}"));
    }

    public function toArray(User $notifiable): array
    {
        return [
            'order_id' => $this->orderId,
            'message' => "Order #{$this->orderId} has been shipped.",
        ];
    }
}

Notice how via() accepts the $notifiable model instance. If a user disabled SMS notifications in their settings, the custom SmsChannel isn't even executed. That saves network calls to external APIs like Twilio or Vonage.

Building Custom Notification Channels

Laravel includes built-in drivers for mail, database, and broadcast, but custom business requirements almost always require custom outputs. Creating a custom channel class in Laravel 12 requires nothing more than a class containing a send() method.

Here is a complete custom SMS driver implementation that handles retry logic gracefully without crashing your job worker.

namespace App\Channels;

use Illuminate\Notifications\Notification;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

class SmsChannel
{
    public function send(object $notifiable, Notification $notification): void
    {
        if (! method_exists($notification, 'toSms')) {
            throw new \RuntimeException('Notification is missing the toSms method.');
        }

        $message = $notification->toSms($notifiable);
        $to = $notifiable->routeNotificationFor('sms', $notification) ?? $notifiable->phone_number;

        if (! $to) {
            return;
        }

        $response = Http::timeout(5)
            ->retry(3, 100)
            ->post('https://api.smsprovider.com/v1/send', [
                'recipient' => $to,
                'body' => $message,
            ]);

        if ($response->failed()) {
            Log::error('SMS delivery failed', [
                'notifiable' => $notifiable->id,
                'status' => $response->status(),
                'body' => $response->body(),
            ]);
        }
    }
}

By default, Laravel looks for a method named routeNotificationForSms() on your User model when using custom string aliases. When passing the channel class directly as SmsChannel::class, calling $notifiable->routeNotificationFor('sms') allows models to define specific routing logic per channel.

Queued Notifications and Uncommitted Database Transactions

The biggest production gotcha with queued notifications occurs when you dispatch a notification inside an active database transaction. If your queue worker picks up the job faster than your primary database node commits the transaction, the worker queries for a model record that doesn't exist yet in its snapshot.

The worker throws an Illuminate\Database\Eloquent\ModelNotFoundException and fails immediately. You'll see jobs failing in Horizon with missing record errors even though the record exists in your database two seconds later.

To fix this in Laravel 12, configure your notification class to wait for transaction completion before sending jobs to the queue. You can enable after_commit globally in your queue config, or add the $afterCommit property directly to the notification class.

class OrderShipped extends Notification implements ShouldQueue
{
    use Queueable;

    public bool $afterCommit = true;
}

This single line drops transient job failures by roughly 95% in high-concurrency systems. If you are processing 500 orders a minute, you cannot afford jobs crashing because database replication lag added 15ms to your commit time.

Broadcasting Real-Time Alerts to Next.js 16 and React 19

When broadcasting notifications, Laravel serializes the output of toArray() or toBroadcast() into a WebSocket payload managed by Laravel Reverb or Pusher. On the frontend, Next.js 16 client components running React 19 can subscribe to private channels using Laravel Echo.

Here is how to set up a React 19 client hook to listen for real-time notifications over WebSockets.

'use client';

import { useEffect, useState } from 'react';
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';

declare global {
  interface Window {
    Pusher: typeof Pusher;
    Echo: Echo;
  }
}

export function useNotifications(userId: number) {
  const [notifications, setNotifications] = useState<Array<any>>([]);

  useEffect(() => {
    if (typeof window === 'undefined') return;

    window.Pusher = Pusher;
    const echo = new Echo({
      broadcaster: 'reverb',
      key: process.env.NEXT_PUBLIC_REVERB_APP_KEY,
      wsHost: process.env.NEXT_PUBLIC_REVERB_HOST,
      wsPort: process.env.NEXT_PUBLIC_REVERB_PORT ?? 80,
      wssPort: process.env.NEXT_PUBLIC_REVERB_PORT ?? 443,
      forceTLS: (process.env.NEXT_PUBLIC_REVERB_SCHEME ?? 'https') === 'https',
      enabledTransports: ['ws', 'wss'],
    });

    echo.private(`App.Models.User.${userId}`)
      .notification((notification: any) => {
        setNotifications((prev) => [notification, ...prev]);
      });

    return () => {
      echo.leave(`App.Models.User.${userId}`);
    };
  }, [userId]);

  return notifications;
}

Laravel broadcasts notifications on private channels formatted as App.Models.User.{id} by default. The React 19 client listens via .notification(), which handles the specific event wrapping Laravel uses behind the scenes. Don't use standard .listen() calls for notifications unless you manually override broadcast names in PHP.

Performance Benchmarks and Real World Bottlenecks

Sending emails synchronously inside an HTTP request lifecycle adds 250ms to 800ms of latency per request. Queueing notifications drops API response times from 450ms down to 35ms on standard 2 vCPU cloud instances running PHP 8.3 with FrankenPHP or Swoole.

However, queueing creates a different bottleneck: memory consumption under heavy batch dispatches. If you send a notification to 50,000 users at once using Notification::send($users, new SystemAlert()), Laravel loads the entire Eloquent collection into memory before dispatching individual queue jobs. Instantiating 50,000 model objects quickly hits your PHP memory limit.

Instead of passing large collections, use database cursors or dispatch dedicated chunking jobs to process users in batches of 1,000.

Common Pitfalls to Avoid

  • Missing Queue Backoffs: Custom HTTP channels hit rate limits if third-party APIs suffer degradation. Always set public $backoff = [10, 30, 60]; on queued notification classes.

  • Database Channel Payload Size: The database channel stores payloads inside a text or json column. Storing full Eloquent models inside toArray() inflates table size and causes slow reads on unindexed notification payloads. Store primitive IDs and brief text strings only.

  • Reverb CORS and Authentication: In Next.js 16, private channel authentication requests land on your Laravel API at /broadcasting/auth. If credentials or cookies aren't forwarded correctly, Echo fails silently without firing callbacks. Ensure your CORS configuration explicitly allows cross-origin credentials.

Md. Mostafijur RahmanMMd. Mostafijur Rahman

WRITTEN BY

Md. Mostafijur Rahman

    Latest Posts

    View All

    Laravel Signed URLs and One-Time Download Links

    Laravel Signed URLs and One-Time Download Links

    Laravel Timezone Handling: UTC, Users, and DST Bugs

    Laravel Timezone Handling: UTC, Users, and DST Bugs

    Laravel Login Throttle: Rate Limiting and Credential Defense

    Laravel Login Throttle: Rate Limiting and Credential Defense

    Building Honest Health Check Endpoints in Laravel

    Building Honest Health Check Endpoints in Laravel

    Writing Production-Ready Laravel Artisan Commands

    Writing Production-Ready Laravel Artisan Commands

    Testing Mail in Laravel: Mailables and Assertions

    Testing Mail in Laravel: Mailables and Assertions

    Solving Low-Priority Queue Starvation in Laravel

    Solving Low-Priority Queue Starvation in Laravel

    Realistic Laravel Seeders with States and Relations

    Realistic Laravel Seeders with States and Relations

    Fixing Laravel Broadcasting Auth and 403 Errors

    Fixing Laravel Broadcasting Auth and 403 Errors

    Laravel Multi Tenancy: Single vs Multi Database

    Laravel Multi Tenancy: Single vs Multi Database