Complete "Using Socket.IO for Rooms, Namespaces, and Auto-Reconnect" first to unlock this lesson.Go to previous lesson

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.

diagram showing two Node.js servers behind a load balancer, each with its own connected clients, and Redis sitting between them as the pub/sub bus

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:

bash
npm install socket.io redis socket.io-redis

Then in your server entry point, create an adapter bound to a Redis client and attach it to the Socket.IO server:

javascript
const http = require('http');
const { Server } = require('socket.io');
const { createAdapter } = require('@socket.io/redis-adapter');
const { createClient } = require('redis');
const httpServer = http.createServer();
const io = new Server(httpServer);
const pubClient = createClient({ url: 'redis://localhost:6379' });
const subClient = pubClient.duplicate();
Promise.all([pubClient.connect(), subClient.connect()]).then(() => {
io.adapter(createAdapter(pubClient, subClient));
io.listen(3000);
});

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:

javascript
io.on('connection', (socket) => {
socket.on('join', (room) => {
socket.join(room);
});
socket.on('message', (room, text) => {
io.to(room).emit('message', {
user: socket.data.username,
text,
time: Date.now()
});
});
});

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.

flowchart showing a message originating on server 1, passing through Redis Pub/Sub, then fanning out to server 2 and server 3 which each deliver it to their local clients

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 practice

Lesson Checkpoint

Review what you learned and get feedback on your work.

0/1
Answered · 0/1
Scaling WebSocket Servers Horizontally with Redis Pub/Sub