Why Redis Pub/Sub Fits WebSocket Fan-Out
Once a WebSocket application runs on more than one server instance, you hit the fundamental cross-instance delivery problem: a message received on server A might need to reach a client whose socket happens to be held by server B, and the two processes have no direct connection to each other. Redis Pub/Sub is a natural fit for solving this because it's simple (two commands, PUBLISH and SUBSCRIBE), fast (Redis processes pub/sub in-memory with sub-millisecond latency for most workloads), and most teams already run Redis for caching or session storage, so adding this responsibility introduces no new infrastructure. Every WebSocket server instance opens a subscriber connection to Redis and listens on one or more channels; when any instance's application code needs to broadcast a message, it calls PUBLISH on the relevant channel, and Redis pushes that message to every currently-subscribed instance, each of which then checks its own local socket table and delivers to any matching connected client.
Cricket analogy: It's like every stadium's scoreboard operator subscribing to a single shared BCCI feed rather than calling each other directly; when Mumbai's ground publishes a wicket update, Chennai's board receives it instantly through the shared feed without a dedicated line between the two grounds.
Architecture Pattern
The typical pattern has three moving parts working together on every server instance: a WebSocket server managing local client connections, a local in-memory map from user/room ID to socket for that instance's own connections, and a Redis client pair (Pub/Sub in Redis requires a dedicated connection for subscribing, since a connection in subscribe mode can't run other commands, which is why libraries like ioredis recommend using two separate client instances, one for publishing and one for subscribing). When a client sends a chat message, the receiving server writes it to the database if needed, then publishes it to a Redis channel scoped to that chat room (e.g., room:42); every server instance subscribed to room:42 receives the published payload and iterates its local socket map, delivering to any client in that room connected to that specific instance. This means a message touches Redis exactly once regardless of how many servers or how many total connected clients exist, which is what makes the pattern scale well even as the cluster grows.
Cricket analogy: It's like every stadium having a local ground announcer (the socket map) plus a shared radio link to the BCCI feed (Redis); when Kohli hits a six, the ground publishes it once to the shared feed, and every stadium's local announcer then relays it only to fans physically present in their own ground.
// Node.js: ws server fanning out chat messages via Redis Pub/Sub
const Redis = require('ioredis');
const pub = new Redis(); // dedicated publisher connection
const sub = new Redis(); // dedicated subscriber connection
const localSockets = new Map(); // userId -> WebSocket, this instance only
sub.subscribe('room:42');
sub.on('message', (channel, payload) => {
const { targetUserIds, data } = JSON.parse(payload);
for (const userId of targetUserIds) {
const socket = localSockets.get(userId);
if (socket && socket.readyState === socket.OPEN) {
socket.send(JSON.stringify(data));
}
}
});
function broadcastToRoom(roomId, targetUserIds, data) {
pub.publish(`room:${roomId}`, JSON.stringify({ targetUserIds, data }));
}Limitations of Redis Pub/Sub
The simplicity of Redis Pub/Sub comes with an important trade-off: it's fire-and-forget, with zero persistence and zero delivery guarantees. If a server instance is disconnected from Redis, restarting, or simply not yet subscribed to a channel at the moment a message is published, that message is gone forever; Redis makes no attempt to buffer it for a late subscriber. This is fine for the WebSocket fan-out use case specifically, since the whole point is delivering to currently-connected clients (a disconnected client wouldn't have received it anyway, and reconnection-and-resync logic, as covered elsewhere, is what handles catching clients up), but it means Redis Pub/Sub is the wrong tool for anything that needs guaranteed delivery, ordering across a restart, or replay, which is what Redis Streams, Kafka, or a proper message queue like RabbitMQ are for. Teams sometimes conflate the two needs and are surprised when Pub/Sub 'loses' messages during a Redis failover or a brief network blip.
Cricket analogy: It's like a stadium's live PA announcement: if you stepped out to the concession stand exactly when the wicket was announced, you simply missed it, there's no replay button on the live PA, unlike the official scorecard archive which permanently records every ball for later lookup.
Redis Pub/Sub messages are not persisted anywhere and are not redelivered after a Redis restart, failover, or a brief subscriber disconnect. If your application needs guaranteed delivery, at-least-once semantics, or the ability to replay recent history to a client that reconnects, use Redis Streams (with consumer groups) or a dedicated message broker instead, and reserve plain Pub/Sub for the specific job of fanning out to currently-connected WebSocket clients.
Presence and Channel Design
Beyond message fan-out, Redis is also the natural home for presence tracking and channel/room membership in this architecture, using its data structures rather than Pub/Sub itself. A common pattern stores each online user as a key with a short TTL (e.g., presence:user:123 set to expire in 30 seconds) refreshed by a heartbeat from the client's connected server every 15-20 seconds; if the heartbeat stops (client disconnected, server crashed), the key simply expires on its own without any explicit cleanup logic needed, which is more resilient than trying to catch every possible disconnect path in application code. Channel design matters too: using one Redis channel per room (room:42) rather than one giant global channel keeps PUBLISH traffic scoped so a server only needs to subscribe to channels for rooms it actually has active connections in, which meaningfully reduces wasted message delivery once you have thousands of concurrent rooms and servers that only host a small fraction of them each.
Cricket analogy: It's like a stadium ticketing system marking a seat as 'occupied' with an auto-expiring hold that refreshes every time the turnstile scanner pings it; if a fan leaves early and stops triggering scans, the seat automatically frees up after a timeout without a steward having to manually check every seat.
For very large deployments (hundreds of thousands of rooms), consider Redis Cluster with channel-name-based sharding, since standard Redis Pub/Sub is single-threaded per node and all PUBLISH throughput for a given channel is bound to whichever node owns it. Also watch for the 'thundering subscribe' pattern where every server subscribes to every possible channel at startup; instead, subscribe dynamically only when a local client actually joins that room, and unsubscribe when the last local client in that room disconnects.
- Redis Pub/Sub solves cross-instance WebSocket delivery: PUBLISH on one server reaches every SUBSCRIBEd server, which then delivers to its own locally-held sockets.
- A dedicated subscriber connection is required per Redis client, since a connection in subscribe mode can't run other Redis commands.
- Each message touches Redis exactly once regardless of cluster size, which is what makes the pattern scale.
- Redis Pub/Sub is fire-and-forget: no persistence, no delivery guarantee, and messages are lost if no one is subscribed at publish time.
- Use Redis Streams, Kafka, or a message queue instead of Pub/Sub when guaranteed delivery or replay is required.
- Presence is best tracked with TTL-based keys refreshed by heartbeats, so stale presence expires automatically without explicit cleanup.
- Per-room channels (rather than one global channel) reduce wasted delivery once a cluster hosts thousands of rooms.
Practice what you learned
1. What problem does Redis Pub/Sub solve for a horizontally-scaled WebSocket deployment?
2. Why do implementations typically use two separate Redis client connections, one for publishing and one for subscribing?
3. What happens to a Redis Pub/Sub message if no server is subscribed to the channel at the moment it's published?
4. Why is a TTL-based presence key refreshed by heartbeats generally more resilient than explicit disconnect-handling code?
5. Why is scoping Redis channels per room (e.g., `room:42`) generally better than using one global channel for all messages?
Was this page helpful?
You May Also Like
Scaling WebSockets Horizontally
How to run WebSocket servers across many instances when every connection is a long-lived, stateful process pinned to one machine.
Backpressure in WebSockets
How to detect and handle slow consumers on a WebSocket connection before unbounded buffering leads to memory exhaustion or unresponsive clients.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebAssembly Study Notes
WebAssembly · 30 topics
Web DevelopmentgRPC Study Notes
Protocol Buffers · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentFlask Study Notes
Python · 30 topics
Web DevelopmentDjango Study Notes
Python · 30 topics
Web DevelopmentNext.js Study Notes
JavaScript · 30 topics