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
redisscaling driver inconfig/reverb.phpto 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 Supervisorminfdsor systemdLimitNOFILE) 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.