Laravel WebSocket Broadcasting with Reverb: Scaling Beyond a Single Worker
#laravel #reverb #websockets #broadcasting #redis

Laravel WebSocket Broadcasting with Reverb: Scaling Beyond a Single Worker

3 min read Mohamed Said Mohamed Said

The Single-Worker Problem

Laravel Reverb is a first-party WebSocket server written in PHP using ReactPHP. Out of the box it runs as a single process, which is fine for development and low-traffic apps. The moment you deploy multiple application servers — or your connection count climbs past a few thousand — you hit a wall: clients connected to server A never receive events published from server B.

The fix is a shared pub/sub backend. Reverb supports Redis as a message bus so every worker node subscribes to the same channel stream.

Enabling Redis Pub/Sub in Reverb

In config/reverb.php, switch the scaling driver:

'servers' => [
    'reverb' => [
        // ...
        'scaling' => [
            'enabled' => true,
            'driver'  => 'redis',
            'connection' => 'default', // your redis connection name
        ],
    ],
],

Reverb uses a dedicated Redis Pub/Sub connection (not the cache or queue connection) so it never blocks your job workers. Make sure the redis extension or predis/predis is available and that your Redis instance allows enough concurrent clients.

Running Multiple Reverb Workers

Each node starts its own Reverb process. Supervisor is the standard approach:

[program:reverb-worker]
command=php /var/www/artisan reverb:start --host=0.0.0.0 --port=8080
autostart=true
autorestart=true
stopwaitsecs=10
numprocs=1
stdout_logfile=/var/log/reverb.log

Behind a load balancer, every node listens on the same port. The load balancer must support sticky sessions (IP hash or cookie-based) for the WebSocket upgrade handshake, but once the connection is established the Redis layer handles cross-node delivery transparently.

Nginx WebSocket Proxy

upstream reverb {
    ip_hash; # sticky sessions for WS upgrade
    server 10.0.0.1:8080;
    server 10.0.0.2:8080;
}

server {
    listen 443 ssl;
    server_name ws.example.com;

    location / {
        proxy_pass http://reverb;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_set_header Host $host;
        proxy_read_timeout 3600s;
    }
}

Tuning Connection Limits

ReactPHP uses a non-blocking event loop, so a single Reverb process can hold thousands of open file descriptors. The OS default ulimit -n is often 1024 — far too low.

Set it in Supervisor:

[program:reverb-worker]
; ...
minfds=65536

Or in your systemd unit:

[Service]
LimitNOFILE=65536

Also raise max_connections in config/reverb.php:

'options' => [
    'tls' => [],
    'max_request_size' => 10_000,
],

Reverb itself does not hard-cap connections by default, but you can add application-level guards via the Reverb\Contracts\ServerFactory if you need per-channel or per-app limits.

Keeping Broadcasts Reliable

Broadcasting from a queued job is the safest pattern. Never broadcast synchronously inside a request when you expect high throughput:

broadcast(new OrderShipped($order))->via('reverb');
// or inside a queued job:
public function handle(): void
{
    broadcast(new OrderShipped($this->order));
}

If the Reverb server is temporarily unreachable, the queued job will retry according to your queue configuration rather than silently dropping the event.

Health Checks

Reverb exposes a simple HTTP health endpoint when started with --debug or via a custom route. Wire it into your load balancer's health check so unhealthy nodes are removed automatically:

curl -f http://10.0.0.1:8080/apps/{app_id}/health

Takeaways

  • Enable the redis scaling driver in config/reverb.php to share state across nodes.
  • Use IP-hash sticky sessions at the load balancer only for the WebSocket upgrade; post-handshake routing is handled by Redis.
  • Raise ulimit -n (via Supervisor minfds or systemd LimitNOFILE) to at least 65 536 per node.
  • Always broadcast from queued jobs so delivery survives transient Reverb restarts.
  • Monitor the Redis pub/sub channel count alongside your application metrics — runaway subscriptions are a common memory leak vector.

Found this useful?

Frequently Asked Questions

3 questions
Q01 Do I need sticky sessions on my load balancer when running multiple Reverb nodes?
Yes, but only for the initial WebSocket upgrade handshake. Once the connection is established, the Redis pub/sub layer ensures events published on any node are delivered to all connected clients regardless of which node they are on.
Q02 Can Reverb replace Pusher entirely in a high-traffic production app?
Yes. With Redis scaling enabled, proper ulimit tuning, and broadcasts dispatched through queued jobs, Reverb handles the same workload as a managed Pusher cluster while keeping all traffic within your own infrastructure.
Q03 What happens to in-flight WebSocket connections when I restart a Reverb worker?
Clients are disconnected and must reconnect. Laravel Echo retries automatically with exponential back-off. Use Supervisor's stopwaitsecs to allow the process to drain gracefully, and deploy rolling restarts across nodes to minimise the visible impact.

Continue reading

More Articles

View all