Laravel · Laravel

Laravel Reverb WebSockets — A Complete Guide

How Laravel Reverb's first-party WebSocket server replaces third-party broadcasting services, what it costs to self-host at scale, and where horizontal scaling still needs Redis.

John Kihiu12 min read

Reverb is Laravel's first-party WebSocket server — a `php artisan reverb:start` process that speaks the same Pusher-protocol broadcasting API Laravel already had, which means most apps that were previously paying for Pusher or running a separate Soketi/Socket.io process can swap the broadcaster driver and keep the exact same `broadcast(new OrderShipped($order))` code. The pitch is simple: self-hosted, first-party, no per-connection billing. The trade-off is that "self-hosted" means you now own the scaling problem a managed service used to handle for you.

Reverb speaks the Pusher protocol on purpose

Reverb didn't invent a new client protocol — it implements the same channel, event, and presence-channel semantics Pusher's JS client already expects, which is why migrating from Pusher is mostly a config change (`BROADCAST_CONNECTION=reverb`, point the Echo client at your Reverb host) rather than a rewrite of every `Echo.channel()` call in the frontend. Private channels still authenticate through your existing `routes/channels.php` authorization callbacks; presence channels still carry member metadata the same way. If your app already used Laravel Echo correctly, Reverb is close to a drop-in swap.

PHP · BROADCASTING WITH REVERB
// .env
BROADCAST_CONNECTION=reverb
REVERB_APP_ID=local
REVERB_APP_KEY=local-key
REVERB_APP_SECRET=local-secret
REVERB_HOST="127.0.0.1"
REVERB_PORT=8080

// routes/channels.php — unchanged from Pusher-based setups
Broadcast::channel('orders.{orderId}', function ($user, $orderId) {
    return $user->can('view', Order::findOrFail($orderId));
});

// Running the server (behind a reverse proxy in production)
// php artisan reverb:start --host=0.0.0.0 --port=8080

A single Reverb process does not scale past one box

Out of the box, `reverb:start` runs one process holding every open WebSocket connection in memory on that machine. That's genuinely fine until you need more connections than one server can hold, or need redundancy so a server restart doesn't drop every connected client at once. Reverb supports horizontal scaling via Redis as a pub/sub backplane — multiple Reverb instances behind a load balancer, all subscribed to the same Redis channels, so a broadcast from any app server reaches every connected client regardless of which Reverb node they're attached to.

Sticky sessions matter for WebSockets, load balancing does not work the normal way

A WebSocket connection is long-lived and stateful — once a client connects to a specific Reverb instance, it needs to stay connected to that instance (or reconnect cleanly if it drops). Round-robin load balancing that could route a single client's traffic across multiple backend instances mid-connection will break things. Configure your load balancer for sticky sessions or connection-based routing, not the round-robin-per-request model that works fine for HTTP.

Scaling cost vs. a managed service

The appeal of self-hosting is avoiding Pusher's per-connection, per-message pricing, but that cost doesn't disappear — it moves to infrastructure and operational burden. Running Reverb at scale means provisioning enough memory for peak concurrent connections, running Redis for the pub/sub backplane, monitoring connection counts and message throughput yourself, and handling failover when a Reverb node dies. For a small-to-mid app, this is a clear win. For a consumer app with unpredictable connection spikes, the operational cost of getting horizontal scaling right can eat into the savings faster than the pricing calculator suggests.

Reverb behind Nginx needs explicit WebSocket upgrade handling

A reverse proxy in front of Reverb must explicitly pass through the Upgrade and Connection headers for the WebSocket handshake to succeed — a default Nginx proxy config written for plain HTTP will silently fail the upgrade and leave clients falling back to polling or failing to connect entirely. This is the single most common "it works locally, not behind our proxy" Reverb issue.

Wrapping up

Reverb is a solid default for real-time features in a Laravel app precisely because it reuses broadcasting code and the Pusher protocol you may already know — the migration cost from an existing Echo-based setup is low. The scaling story is real but not free: plan for Redis-backed horizontal scaling and sticky-session load balancing before you need them, and go in accepting that self-hosting trades subscription cost for infrastructure ownership, not for free capacity.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.