Why the Default Reverb Setup Breaks Under Load
Laravel Reverb ships with sensible defaults that work perfectly for demos and small apps. The moment you push beyond a few hundred concurrent connections or start broadcasting from high-frequency jobs, you hit a predictable set of problems: queue workers become the bottleneck, channel authorization adds latency on every subscribe, and a single Reverb process becomes a single point of failure.
This article focuses on three concrete production concerns: relieving queue pressure from broadcast events, hardening channel authorization, and running Reverb horizontally.
Decoupling Broadcast Events from the Request Cycle
Every ShouldBroadcast event dispatched inside a request is serialized and pushed onto a queue. If your broadcast queue shares workers with your default queue, a spike in HTTP traffic can starve WebSocket delivery.
Use a Dedicated Broadcast Queue
// app/Events/OrderStatusUpdated.php
class OrderStatusUpdated implements ShouldBroadcastNow
{
public function broadcastQueue(): string
{
return 'broadcasting';
}
}
Then in config/horizon.php, give that queue its own supervisor pool:
'broadcasting' => [
'connection' => 'redis',
'queue' => ['broadcasting'],
'balance' => 'auto',
'minProcesses' => 2,
'maxProcesses' => 10,
'tries' => 3,
],
Using ShouldBroadcastNow skips the queue entirely for ultra-low-latency events where the payload is cheap to compute. Reserve it for lightweight events only — anything that hits the database should stay queued.
Channel Authorization Without the N+1 Trap
Private and presence channels call your authorization callbacks on every subscribe. If those callbacks issue unguarded Eloquent queries, a reconnect storm (e.g., after a deploy) will hammer your database.
Cache Authorization Results
// routes/channels.php
Broadcast::channel('orders.{orderId}', function (User $user, int $orderId) {
$cacheKey = "channel_auth:{$user->id}:{$orderId}";
return Cache::remember($cacheKey, now()->addMinutes(5), function () use ($user, $orderId) {
return $user->can('view', Order::findOrFail($orderId));
});
});
Five minutes is usually safe for order-level permissions. For presence channels that carry user metadata, cache the payload too:
Broadcast::channel('team.{teamId}', function (User $user, int $teamId) {
$member = Cache::remember(
"presence_auth:{$user->id}:{$teamId}",
now()->addMinutes(2),
fn () => $user->teams()->find($teamId)
);
return $member ? ['id' => $user->id, 'name' => $user->name] : false;
});
Horizontal Scaling with a Shared Redis Pub/Sub Backend
Reverb uses Redis pub/sub to fan out messages across multiple server instances. The key is ensuring every Reverb worker subscribes to the same Redis channel namespace.
# .env on each node
REVERB_SERVER_HOST=0.0.0.0
REVERB_SERVER_PORT=8080
REVERB_SCALING_ENABLED=true
REVERB_SCALING_REDIS_HOST=redis-cluster.internal
Behind a load balancer, sticky sessions (IP hash or cookie-based) are not required when scaling is enabled — Reverb's Redis backend propagates messages to whichever node holds the connection. Confirm this in config/reverb.php:
'scaling' => [
'driver' => 'redis',
'redis' => [
'connection' => 'reverb',
],
],
Define a dedicated Redis connection in config/database.php so Reverb's pub/sub traffic doesn't share a connection pool with your cache or session drivers.
Monitoring What Actually Matters
Reverb exposes a /apps/{appId}/connections endpoint (protected by your app secret) that returns current connection counts. Scrape it with a cron and push to your metrics store, or hook into Reverb's ConnectionEstablished and ConnectionClosed events:
Event::listen(\Laravel\Reverb\Events\ConnectionEstablished::class, function ($event) {
Metrics::increment('reverb.connections.active');
});
Track queue depth on the broadcasting queue separately from connection count — a growing queue with stable connections means your event payload computation is the bottleneck, not Reverb itself.
Takeaways
- Isolate broadcast events onto a dedicated Horizon queue pool to prevent starvation.
- Cache channel authorization callbacks; reconnect storms will expose any unguarded query.
- Enable
REVERB_SCALING_ENABLEDwith a dedicated Redis connection for true horizontal scale. - Use
ShouldBroadcastNowonly for lightweight, database-free events. - Monitor queue depth and connection count independently — they point to different bottlenecks.