Scaling Socket.IO with the Redis Adapter: Practical Setup and Trade‑offs
Learn how to horizontally scale Socket.IO with the Redis adapter: minimal code, NGINX sticky sessions, verification commands, and the operational trade‑offs you’ll face in production.
07 Mar 2026, 11:30 UTC

The problem: a single‑process Socket.IO server can’t share rooms
Socket.IO’s default in‑memory adapter only knows about sockets that live in the same Node.js process. When you add a second instance behind a load balancer, a client that connects to instance A will never receive events broadcast from instance B — rooms are isolated per process.
Thesis: the @socket.io/redis-adapter (v8+) fans out join/leave and broadcast operations through Redis Pub/Sub, letting any number of Node.js workers share the same logical rooms.
You still need sticky sessions for the initial handshake and HTTP long‑polling fallback, but after that the adapter makes the cluster behave like a single server.
Minimal working configuration
1. Install the adapter (run on each app server)
npm i @socket.io/redis-adapter@^8 ioredis@^5
Verify the version you actually got:
npm ls @socket.io/redis-adapter
# expect @socket.io/redis-adapter@8.x.x
2. Create a shared Redis client and attach the adapter
// server.js (run on every Node instance)
const { createServer } = require('http');
const { Server } = require('socket.io');
const { createAdapter } = require('@socket.io/redis-adapter');
const Redis = require('ioredis');
const pubClient = new Redis({ host: '', port: 6379 });
const subClient = pubClient.duplicate();
const httpServer = createServer();
const io = new Server(httpServer, { /* your options */ });
io.adapter(createAdapter(pubClient, subClient));
io.on('connection', (socket) => {
socket.join('room1');
socket.on('chat', (msg) => io.to('room1').emit('chat', msg));
});
httpServer.listen(3000, () => console.log('Socket.IO listening on :3000'));
Replace <redis-host> with your Redis endpoint. The same file runs on every worker; each process gets its own pubClient/subClient pair.
3. Load balancer with sticky sessions (NGINX example)
upstream socketio_backend {
ip_hash; # sticky by client IP
server app1:3000;
server app2:3000;
}
server {
listen 80;
location / {
proxy_pass http://socketio_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400;
}
}
ip_hash (or a cookie‑based affinity) guarantees the WebSocket handshake and any subsequent polling requests hit the same instance.
Worked example: a message crosses the cluster
- Client C1 connects to Server A (NGINX routes via
ip_hash). - C1 emits
chat→ Server A callsio.to('room1').emit('chat', msg). - The adapter publishes a Redis message on channel
socket.io#/#room1#. - Server B receives the Pub/Sub message, looks up its local sockets in
room1, and delivers the event to C2 (connected to Server B).
You can see the traffic with redis-cli MONITOR (run on the Redis host, requires MONITOR permission):
redis-cli MONITOR
# look for PUBLISH "socket.io#/#room1#" "{\"type\":2,...}"
After a broadcast, list active adapter channels to confirm both servers are subscribed:
redis-cli PUBSUB CHANNELS "socket.io*"
# should return at least two channel names
Trade‑offs and operational limits
- Redis is a single point of failure. Deploy Redis Sentinel or Redis Cluster and monitor Pub/Sub lag (e.g.,
redis-cli INFO stats | grep pubsub). - Message ordering across servers isn’t guaranteed. Events originating on different workers may arrive at a client out of order.
- Binary payloads are base64‑encoded when they traverse Redis Pub/Sub, adding ~33 % bandwidth overhead.
- Orphaned room keys (prefixed
socket.io#) remain after a hard crash (kill -9). Schedule a TTL‑based cleanup job or useSCAN+DELperiodically. - Sticky‑session misconfiguration shows up as
400 Session ID unknownduring polling fallback. Verify NGINXip_hashor cookie affinity before debugging application code.
Verification checklist you can run today
- Version check –
npm ls @socket.io/redis-adapteron each app server (needs read access tonode_modules). - Channel health –
redis-cli PUBSUB CHANNELS "socket.io*"from a Redis CLI (requiresPUBSUBpermission). - Live broadcast trace –
redis-cli MONITORwhile triggering a chat message (run on Redis host, may produce high output). - Failure simulation –
kill -9 <pid-of-server-A>(requires root or same‑user permission) then confirm Server B still delivers to all clients inroom1. - Load test –
autocannon -c 200 -d 30 -w 10 http:///socket.io/(run from a separate load‑gen machine) while watching Redis CPU/network.
If all checks pass, you have a horizontally scalable Socket.IO deployment that survives a single worker loss without losing room membership.
Next steps
Add Redis Sentinel for automatic failover, instrument Pub/Sub latency in your metrics stack, and consider io.volatile.emit for high‑frequency, loss‑tolerant streams (e.g., cursor positions). The adapter’s fetchSockets() and serverSideEmit() (v4.7+) enable cross‑process RPC but add a Redis round‑trip — use them sparingly.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.