# Scaling WebSockets with Redis Pub/Sub: From One Server to Many

> How to horizontally scale a WebSocket server using Redis Pub/Sub and Socket.IO — covering the sticky session problem, Redis adapter setup, and what happens when Redis becomes the bottleneck.

URL: https://www.matthewswong.com/en/blog/websocket-scaling-redis-pubsub/

- Author: Matthews Wong (https://www.matthewswong.com/en/about/)
- Published: 2025-06-10
- Updated: 2025-06-10
- Category: DevOps

A single Node.js server can handle 500,000+ idle WebSocket connections with proper tuning. But the moment you add a second server instance, you hit the fundamental WebSocket scaling problem: clients connected to Server 1 can't receive messages from Server 2 because their sockets live in different processes. Redis Pub/Sub solves this by acting as the message backplane between server instances — when any server needs to broadcast a message, it publishes to Redis, and all servers subscribe and fan out to their local connections. This guide covers the full implementation in NestJS with Socket.IO.

## The WebSocket Scaling Problem

WebSockets are stateful — unlike HTTP requests, they maintain a persistent connection to a specific server instance. In a multi-server deployment behind a load balancer, a client is connected to exactly one server. When you want to send a message to a user who might be connected to any of your servers, you need a backplane that coordinates between servers. Without it, you'd need to send the message to every server and let each server fan out to clients — exactly what Redis Pub/Sub enables. The naive alternative — always routing the same user to the same server (sticky sessions) — works but fails when server instances restart or scale in.

### Sticky Sessions: A Partial Solution

Sticky sessions (also called session affinity) configure the load balancer to route the same client to the same server based on their IP or a cookie. This avoids the backplane requirement for direct client communication, but it doesn't solve the case where one server needs to send a message to a client on another server (which happens in any multi-user collaboration or broadcast scenario). Sticky sessions also break when a server restarts — clients must reconnect and may hit a different server, requiring state reconstruction. Use sticky sessions as a fallback during Redis failure, not as your primary scaling strategy.

> From building real-time notification features: use Socket.IO rooms to group clients by user ID. When you need to send a notification to a specific user, emit to their room (io.to(userId).emit(...)) rather than tracking socket IDs. The Redis adapter ensures that room membership is replicated across all server instances, so emitting to a room works regardless of which server the user's socket is on. This pattern handles multiple browser tabs from the same user correctly — all tabs join the same user room.

## Redis Pub/Sub Adapter for Socket.IO

Socket.IO's @socket.io/redis-adapter package replaces the default in-memory adapter with a Redis-backed one. All server instances connect to the same Redis instance and subscribe to a shared channel. When Server 1 emits to a room or broadcasts, it publishes to Redis. All servers (including Server 1) receive the publication and fan out to locally connected clients in that room. The setup is a three-line change from the default Socket.IO server configuration — create a Redis publisher client, a subscriber client, and call adapter(createAdapter(pubClient, subClient)).

### NestJS WebSocket Gateway Setup

In NestJS, WebSocket servers are implemented as Gateways — classes decorated with @WebSocketGateway. The gateway uses Socket.IO under the hood by default. To add the Redis adapter, override the afterInit hook in your gateway and set the adapter programmatically, or configure it at the application level in main.ts. The gateway can subscribe to application events (from NestJS EventEmitter or CQRS EventBus) and forward them to connected clients via socket emissions — this is how server-side events (invoice approved, document updated) reach the browser in real time.

> **Redis Pub/Sub is not infinite — it has a throughput ceiling**

> Redis Pub/Sub is not magic. When you start broadcasting millions of messages per second across 100K+ sockets, Redis can become the bottleneck. Each message published to a channel is delivered to every subscriber (all your server instances) — the per-subscriber fan-out cost multiplies with server count. At very high scale, switch to Redis Cluster, use NATS JetStream (built for high-throughput pub/sub), or Kafka for the backplane. Also, Redis Pub/Sub is fire-and-forget — messages published while a subscriber is disconnected are lost. For reliable message delivery to WebSocket clients (notifications they must not miss), persist the notification in the database and deliver it on reconnect.

## Connection Management and Reconnection

WebSocket connections drop — mobile users switch between WiFi and cellular, browser tabs sleep, servers restart. Build reconnection logic into your client. Socket.IO's client handles reconnection automatically with exponential backoff. On reconnect, the server should replay any missed messages. Implement a 'last seen' cursor: when a client reconnects, it sends its last received message timestamp, and the server queries missed notifications from the database and delivers them before resuming live events. Without this, reconnecting clients see a gap in real-time updates.

## Horizontal Scaling in Practice

The deployment architecture for a horizontally scaled WebSocket server: load balancer (Nginx or cloud LB) → multiple NestJS Gateway instances → shared Redis Pub/Sub backplane. Configure the load balancer for sticky sessions as a safety net (in case the Redis adapter is temporarily unavailable). Run Redis in a High Availability configuration (Sentinel or Cluster) — if Redis goes down, all WebSocket communication breaks. Monitor Redis connection count, memory usage, and Pub/Sub message rate as leading indicators of backplane health. Scale NestJS instances horizontally when per-instance CPU or memory is consistently above 70%.

## Alternatives to Redis for WebSocket Scaling

Redis Pub/Sub works well for moderate scale (tens of thousands of concurrent connections, millions of messages per day). For higher scale, consider: NATS (purpose-built messaging system with lower overhead than Redis for Pub/Sub, supports JetStream for persistent messaging), Kafka (for very high throughput with durable message history), or a managed WebSocket service like Ably or Pusher (trade operational control for managed scale). For an ERP or SaaS app with up to 10K concurrent users, Redis Pub/Sub is more than sufficient and avoids adding new infrastructure.

## Sources & Further Reading

- Socket.IO Docs — Redis Adapter
- Ably — Scaling Pub/Sub with WebSockets and Redis
- WebSocket.org — WebSockets at Scale: Architecture for Millions of Connections

## Source links

- https://socket.io/docs/v4/redis-adapter/
- https://ably.com/blog/scaling-pub-sub-with-websockets-and-redis
- https://websocket.org/guides/websockets-at-scale/

## FAQ

### Why can't you just use sticky sessions instead of Redis Pub/Sub for WebSocket scaling?

Sticky sessions route each client to the same server, which avoids the cross-server messaging problem for direct client communication. However, they break whenever a server restarts — clients reconnect and may land on a different server, losing state. They also don't solve the case where one server needs to send a message to a client connected to another server, which happens in any multi-user or broadcast scenario.

### How does the Redis Pub/Sub adapter work with Socket.IO?

The @socket.io/redis-adapter package replaces Socket.IO's default in-memory adapter with a Redis-backed one. Every server instance connects to the same Redis instance and subscribes to a shared channel. When any server emits to a room or broadcasts, it publishes to Redis, and all server instances receive that publication and fan out to their locally connected clients in the relevant room.

### What happens to WebSocket messages if Redis goes down?

Redis Pub/Sub is fire-and-forget — messages published while a subscriber is disconnected are lost. If Redis goes down entirely, all cross-server WebSocket communication breaks. The post recommends running Redis in a High Availability configuration (Sentinel or Cluster) and treating sticky sessions as a fallback safety net during Redis failure rather than the primary strategy.

### How should you handle notifications that clients must not miss when WebSocket connections drop?

Because WebSocket connections can drop at any time (mobile network switches, tab sleep, server restarts), the post recommends persisting notifications in the database. On reconnect, the client sends its last-received message timestamp and the server queries missed notifications from the database before resuming live events — a pattern called a 'last seen' cursor.

### When does Redis Pub/Sub stop being the right backplane for WebSocket scaling?

Redis Pub/Sub works well for moderate scale — tens of thousands of concurrent connections and millions of messages per day. At very high throughput (millions of messages per second across 100K+ sockets), every published message is delivered to every subscriber, so the fan-out cost multiplies with server count and Redis becomes the bottleneck. At that point the post suggests switching to Redis Cluster, NATS JetStream, or Kafka.

---

- HTML version: https://www.matthewswong.com/en/blog/websocket-scaling-redis-pubsub/
- Bahasa Indonesia: https://www.matthewswong.com/id/blog/websocket-scaling-redis-pubsub/
- Site index for agents: https://www.matthewswong.com/llms.txt
- Sitemap: https://www.matthewswong.com/sitemap.xml
- API description (OpenAPI): https://www.matthewswong.com/openapi.json
