Laravel Reverb WebSocket Broadcasting: Real-Time Channels, Auth, and Scaling Patterns
#laravel #reverb #websockets #broadcasting #real-time

Laravel Reverb WebSocket Broadcasting: Real-Time Channels, Auth, and Scaling Patterns

4 min read Mohamed Said Mohamed Said

Beyond the Hello-World: Laravel Reverb in Production

Laravel Reverb ships as a first-party WebSocket server, but most tutorials stop at php artisan reverb:start and a public channel. Real applications need private channels with proper authorization, presence channels for collaborative UIs, and a deployment story that survives more than one server.

Channel Types and When to Use Each

| Channel | Auth required | Presence data | Typical use | |---------|--------------|---------------|-------------| | Public | No | No | Public feeds, announcements | | Private | Yes | No | Per-user notifications, order updates | | Presence| Yes | Yes | Collaborative editors, live cursors |

Defining Private and Presence Channels

Channel routes live in routes/channels.php. Return true/false for private channels; return an array of user metadata for presence channels — Reverb forwards that payload to every subscriber.

// routes/channels.php

use App\Models\Project;
use Illuminate\Support\Facades\Broadcast;

// Private: only the owning user may subscribe
Broadcast::channel('orders.{orderId}', function ($user, int $orderId) {
    return $user->orders()->where('id', $orderId)->exists();
});

// Presence: return metadata so the UI knows who is online
Broadcast::channel('projects.{projectId}', function ($user, int $projectId) {
    $project = Project::find($projectId);

    if (! $project?->members()->where('user_id', $user->id)->exists()) {
        return false;
    }

    return [
        'id'     => $user->id,
        'name'   => $user->name,
        'avatar' => $user->avatar_url,
    ];
});

Broadcasting an Event on a Presence Channel

// app/Events/CursorMoved.php

use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;

class CursorMoved implements ShouldBroadcast
{
    public function __construct(
        public readonly int   $projectId,
        public readonly int   $userId,
        public readonly float $x,
        public readonly float $y,
    ) {}

    public function broadcastOn(): array
    {
        return [new PresenceChannel("projects.{$this->projectId}")];
    }

    // Only broadcast the payload the client actually needs
    public function broadcastWith(): array
    {
        return ['user_id' => $this->userId, 'x' => $this->x, 'y' => $this->y];
    }

    // Throttle: drop duplicate events within the same second
    public function broadcastWhen(): bool
    {
        return true; // add cache-based throttle here if needed
    }
}

Tuning the Reverb Server

Reverb's config lives in config/reverb.php. The most impactful knobs for production:

'servers' => [
    'reverb' => [
        'host'    => env('REVERB_SERVER_HOST', '0.0.0.0'),
        'port'    => env('REVERB_SERVER_PORT', 8080),
        'options' => [
            // Raise open-file limits before touching this
            'max_connections'  => 10_000,
            // Keep idle connections alive; lower = faster cleanup
            'max_request_size' => 10_000, // bytes
        ],
    ],
],

Pair this with a systemd unit that sets LimitNOFILE=65535 so the OS doesn't cap you first.

Horizontal Scaling with Redis Pub/Sub

A single Reverb process is a single point of failure. Run multiple workers behind a load balancer and let Redis fan out messages between them:

// config/reverb.php  (scaling section)
'scaling' => [
    'enabled' => true,
    'driver'  => 'redis',
    'connection' => env('REVERB_SCALING_CONNECTION', 'default'),
],

Each Reverb worker subscribes to the same Redis channel. When worker A receives a broadcast, Redis delivers it to workers B and C so their connected clients all receive the event — no sticky sessions required.

Tip: Use a dedicated Redis connection for Reverb scaling, separate from your cache and queue connections, to avoid head-of-line blocking under load.

Client-Side Authentication Flow

Echo's auth endpoint hits /broadcasting/auth by default. Ensure your API middleware group includes auth:sanctum (or your guard of choice) and that CORS allows the origin your frontend runs on:

import Echo from 'laravel-echo';
import Pusher from 'pusher-js';

window.Echo = new Echo({
    broadcaster: 'reverb',
    key: import.meta.env.VITE_REVERB_APP_KEY,
    wsHost: import.meta.env.VITE_REVERB_HOST,
    wsPort: import.meta.env.VITE_REVERB_PORT,
    forceTLS: false,
    enabledTransports: ['ws'],
    authEndpoint: '/broadcasting/auth',
});

window.Echo
    .join(`projects.${projectId}`)
    .here(members  => console.log('online:', members))
    .joining(member => console.log('joined:', member))
    .leaving(member => console.log('left:',   member))
    .listen('CursorMoved', e => moveCursor(e));

Key Takeaways

  • Private channels return a boolean; presence channels return a metadata array — both go through routes/channels.php.
  • Keep broadcastWith() lean; every connected client receives the full payload.
  • Enable Redis scaling before you need it — retrofitting under load is painful.
  • Separate the Reverb Redis connection from cache/queue to prevent contention.
  • Set OS-level file descriptor limits (LimitNOFILE) before raising max_connections.

Found this useful?

Frequently Asked Questions

3 questions
Q01 Do I need sticky sessions when running multiple Reverb workers behind a load balancer?
No. With Redis scaling enabled, each worker subscribes to a shared Redis pub/sub channel. A broadcast published to any worker is fanned out to all workers, so any client can connect to any worker without session affinity.
Q02 How does Reverb's channel authorization differ from a standard HTTP middleware check?
Channel authorization runs through the `/broadcasting/auth` endpoint, which invokes the closure registered in `routes/channels.php`. The closure receives the authenticated user and any route parameters extracted from the channel name, letting you perform Eloquent queries or policy checks just as you would in a controller.
Q03 What is the practical difference between ShouldBroadcast and ShouldBroadcastNow?
ShouldBroadcast dispatches the broadcast through your queue, so it respects queue workers and backpressure. ShouldBroadcastNow bypasses the queue and broadcasts synchronously during the HTTP request — useful for low-latency events where queue delay is unacceptable, but it adds latency to the originating request.

Continue reading

More Articles

View all