Laravel Reverb in Production: Scaling WebSockets Beyond a Single Server
#laravel #reverb #websockets #redis #broadcasting

Laravel Reverb in Production: Scaling WebSockets Beyond a Single Server

4 min read Mohamed Said Mohamed Said

Why Reverb Needs Special Attention in Production

Laravel Reverb is a native PHP WebSocket server built on ReactPHP's event loop. It handles long-lived TCP connections, which means the usual stateless assumptions you make about HTTP workers no longer apply. A single Reverb process holds connection state in memory. The moment you add a second node, clients on node A can't receive events published by a job running on node B — unless you wire up a shared pub/sub backend.

This article covers the exact configuration needed to run Reverb reliably across multiple nodes.


The Core Problem: In-Memory Channel State

By default, Reverb tracks which WebSocket connections are subscribed to which channels entirely in the current process's memory. When a queued job calls:

broadcast(new OrderShipped($order));

Laravel publishes the event to the configured broadcasting driver. If that driver is reverb, the event hits one Reverb node via HTTP. Any client connected to a different node never sees it.


Solution: Redis as the Pub/Sub Backbone

Reverb ships with a Redis scaling driver. Enable it in config/reverb.php:

'servers' => [
    'reverb' => [
        'driver' => 'reverb',
        'key'    => env('REVERB_APP_KEY'),
        // ...
        'scaling' => [
            'enabled' => true,
            'driver'  => 'redis',
            'connection' => 'default', // your redis connection name
        ],
    ],
],

With this enabled, every Reverb node subscribes to a shared Redis channel. When node A receives a broadcast, it publishes to Redis; node B picks it up and fans the message out to its own connected clients.

Important: Use a dedicated Redis connection for Reverb scaling — not the same one your cache or queue uses. Connection contention under load will cause missed messages.


Nginx: Sticky Sessions Are Non-Negotiable

WebSocket upgrades require the same TCP connection to persist for the lifetime of the session. You must configure your load balancer to route a given client to the same upstream for the duration of the connection.

With Nginx and ip_hash:

upstream reverb_nodes {
    ip_hash;
    server 10.0.1.10:8080;
    server 10.0.1.11:8080;
    server 10.0.1.12:8080;
}

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

    location / {
        proxy_pass http://reverb_nodes;
        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;
    }
}

ip_hash is a blunt instrument — prefer cookie-based stickiness (sticky module) or an AWS ALB with stickiness enabled if you need finer control.


Supervisor: Keeping Reverb Alive

Reverb is a long-running process. Supervisor is the simplest way to manage it:

[program:reverb]
command=php /var/www/artisan reverb:start --host=0.0.0.0 --port=8080
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
stdout_logfile=/var/log/reverb.log
stderr_logfile=/var/log/reverb-error.log

Set autorestart=true and monitor the log for ReactPHP loop exceptions — an unhandled exception in a coroutine can silently kill the loop without crashing the process.


Health Checks and Graceful Deploys

Reverb exposes no built-in HTTP health endpoint, but you can add a lightweight TCP check. For zero-downtime deploys:

  1. Start new Reverb processes on the new release.
  2. Drain old connections by removing old nodes from the upstream pool.
  3. Wait for proxy_read_timeout to expire naturally, or send SIGTERM and let existing connections close.

Avoid SIGKILL — it drops all open WebSocket connections immediately.


Takeaways

  • Enable Reverb's Redis scaling driver on any multi-node deployment; in-memory state does not survive across processes.
  • Use a dedicated Redis connection for Reverb to avoid contention with cache and queue workloads.
  • Configure sticky sessions at the load balancer — WebSocket upgrades require connection affinity.
  • Run Reverb under Supervisor with autorestart=true and monitor for silent event-loop failures.
  • Plan graceful draining during deploys; SIGKILL drops all live connections instantly.

Found this useful?

Frequently Asked Questions

3 questions
Q01 Do I need Redis if I only run a single Reverb node?
No. With a single node, all connection state lives in one process and broadcasts reach every connected client. Redis scaling is only required when you run two or more Reverb processes, whether on the same machine or different servers.
Q02 Can I run Reverb under Laravel Octane for better performance?
Reverb already runs its own ReactPHP event loop and is not designed to run inside an Octane worker. They are separate long-running processes. Run Reverb with its own Supervisor program alongside your Octane workers.
Q03 How do I monitor the number of active WebSocket connections per node?
Reverb does not expose Prometheus metrics out of the box. You can query the Reverb HTTP API (enabled in config) for connection counts, or push custom metrics from within a scheduled command that calls the Reverb manager to count active channels.

Continue reading

More Articles

View all