The Problem with a Single Server
Every WebSocket connection lives in the memory of one Node.js process. If you run a single server, all clients connect to it and you can broadcast a message with a simple in-memory loop. The moment you run a second instance behind a load balancer, clients are split across processes. A message that arrives at server A can never reach a client connected to server B, because the two processes share no memory.
Horizontal scaling therefore needs a way for servers to talk to each other. Redis Pub/Sub is the lightest option: every server subscribes to a set of channels, and when one server publishes, Redis fans the message out to every subscriber.
Wiring Up a Redis Adapter
The `socket.io-redis` adapter plugs into Socket.IO's built-in adapter API and replaces the default in-memory broadcast with Redis Pub/Sub. Install it on every instance:
Then in your server entry point, create an adapter bound to a Redis client and attach it to the Socket.IO server:
Once attached, every call to `io.emit`, `io.to(room).emit`, or `socket.broadcast.emit` goes through Redis. Servers subscribed to the same Redis instance will deliver the event to their own connected sockets, no matter which server originated it.
Broadcasting Across Instances
With the adapter in place, your broadcast code does not change. A chat room handler that works on one server works on ten:
When client A on server 1 emits `message`, Socket.IO serializes the event, publishes it to Redis on a room-scoped channel, and every other server picks it up and fans it out to its local sockets in that room. From the client's perspective, the chat is real-time and global.
Scaling the Adapter Beyond Two Nodes
The pattern above scales linearly. A few operational rules keep it healthy at higher node counts.
First, run Redis with reasonable persistence. Pub/Sub messages are not stored, but the adapter also uses Redis keys for handshake state. A flush on crash will force clients to reconnect, which is acceptable for chat but not for stateful workloads.
Second, put sticky sessions on your load balancer for the WebSocket upgrade only, or accept the cost of reconnects. Without stickiness, a client that reconnects may land on a different server and have to rejoin rooms. Socket.IO handles this transparently because room membership is server-local, but the client briefly misses events during the handshake.
Third, monitor the Redis Pub/Sub channel count and subscriber count. Each Socket.IO namespace adds channels, so a single deployment with several namespaces multiplies the channel footprint.
Finally, if Redis becomes the bottleneck, you can shard Pub/Sub across a Redis Cluster. The `@socket.io/redis-adapter` package supports cluster mode out of the box by accepting multiple connection options.
Common Pitfalls
A frequent mistake is creating the adapter before both Redis clients have connected. Publishing on an unconnected client throws or silently drops the message. Always `await` the connect calls, or wrap them in a ready callback as shown above.
Another mistake is using a single Redis client for both publish and subscribe. Redis requires a dedicated subscriber connection because a client in subscribe mode cannot run normal commands. The `pubClient.duplicate()` call in the example creates that second connection for you.
A subtler issue is relying on `io.local` for cross-server logic. `io.local.emit` only fires on the current server and bypasses Redis, which is useful for per-instance diagnostics but breaks the moment you assume the message reached every node. Use plain `io.emit` for anything that must reach all clients.
Summary
Horizontal scaling of WebSocket servers comes down to two pieces: a load balancer in front of N Node.js instances, and Redis Pub/Sub as the message bus between them. The `socket.io-redis` adapter wraps the bus so application code stays unchanged. Rooms, broadcasts, and namespaces all work across instances the same way they work on one. The main operational concerns are Redis availability, sticky sessions for WebSocket upgrades, and never confusing `io.local.emit` with `io.emit`.
Lesson Checkpoint
Review what you learned and get feedback on your work.