Why Reverb Changes the WebSocket Story for Laravel
Before Reverb, running WebSockets in Laravel meant operating a separate Node.js process (Pusher-compatible servers like Soketi or Laravel Echo Server) alongside your PHP app. Reverb collapses that into a single PHP binary, shares your existing service container, and integrates natively with Laravel's broadcasting system. That simplicity is real — but production deployments surface a set of concerns that the getting-started docs gloss over.
Deployment Architecture
Reverb runs as a long-lived Ratchet/ReactPHP process. It is not a traditional PHP-FPM request; it is a persistent server. That changes how you deploy it.
# Start Reverb (production)
php artisan reverb:start --host=0.0.0.0 --port=8080 --env=production
Manage it with Supervisor so it restarts on failure:
[program:reverb]
command=php /var/www/html/artisan reverb:start --host=0.0.0.0 --port=8080
autostart=true
autorestart=true
stopwaitsecs=10
user=www-data
stdout_logfile=/var/log/reverb.log
Nginx proxies WebSocket upgrades to Reverb while your normal FPM traffic continues on port 80/443:
location /app {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_read_timeout 3600;
}
Horizontal Scaling with Redis
A single Reverb process is bound to one machine. When you scale to multiple app servers, a client connected to server A won't receive events broadcast from server B. The fix is the Redis scaling driver, which uses Pub/Sub to fan out events across all Reverb nodes.
// config/reverb.php
'scaling' => [
'driver' => 'redis',
'connection' => 'default', // your Redis connection from database.php
],
Each Reverb node subscribes to the same Redis channel. When your application fires a broadcast event, any node can pick it up and push it to the connected clients it owns. This requires sticky sessions at your load balancer (or a session-affinity cookie) so that the WebSocket handshake and subsequent frames always hit the same node.
Gotcha: If you skip sticky sessions, the HTTP-based channel auth endpoint (
/broadcasting/auth) may be handled by a different server than the one holding the WebSocket connection. The auth will succeed, but Reverb on the other node won't have the subscription registered. Always pair Redis scaling with L4/L7 sticky routing.
Private and Presence Channel Auth
Reverb reuses Laravel's existing BroadcastServiceProvider and channel definitions:
// routes/channels.php
Broadcast::channel('orders.{orderId}', function (User $user, int $orderId) {
return $user->can('view', Order::findOrFail($orderId));
});
Broadcast::channel('presence.team.{teamId}', function (User $user, int $teamId) {
if ($user->belongsToTeam($teamId)) {
return ['id' => $user->id, 'name' => $user->name];
}
});
Presence channels return an array on success; that payload becomes the member data visible to all subscribers. Returning false or null denies access.
On the frontend with Echo:
window.Echo.join(`presence.team.${teamId}`)
.here(members => console.log('Online:', members))
.joining(member => console.log('Joined:', member.name))
.leaving(member => console.log('Left:', member.name));
Connection Limits and Tuning
Reverb exposes connection limits per application in config/reverb.php. Under load, the bottleneck is usually open file descriptors, not CPU. Raise the OS limit:
# /etc/security/limits.conf
www-data soft nofile 65535
www-data hard nofile 65535
Also tune the max_connections and max_request_size settings:
'apps' => [[
'id' => env('REVERB_APP_ID'),
'key' => env('REVERB_APP_KEY'),
'secret' => env('REVERB_APP_SECRET'),
'options' => [
'host' => env('REVERB_HOST', '0.0.0.0'),
'port' => env('REVERB_PORT', 8080),
'max_message_size' => 10_000, // bytes
],
]],
Key Takeaways
- Reverb is a persistent PHP process — manage it with Supervisor, not FPM.
- Use the Redis scaling driver for multi-node deployments; pair it with sticky sessions at the load balancer.
- Channel auth runs through your normal Laravel HTTP stack — policies and gates work as expected.
- Presence channel member data is the array returned from the channel callback, not a separate API call.
- Raise OS file descriptor limits before you hit connection ceilings in production.
max_message_sizeis your first line of defense against oversized payloads crashing the event loop.