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:
- Start new Reverb processes on the new release.
- Drain old connections by removing old nodes from the upstream pool.
- Wait for
proxy_read_timeoutto expire naturally, or sendSIGTERMand 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=trueand monitor for silent event-loop failures. - Plan graceful draining during deploys;
SIGKILLdrops all live connections instantly.