Scaling & Reliability
Multi-Instance Scaling with the Redis Adapter
By default, room membership and broadcasts only work within one server process's memory — a client on server A won't receive a broadcast triggered on server B. Scaling horizontally requires an adapter (commonly Redis-backed) so every instance shares events over pub/sub.
const { createAdapter } = require('@socket.io/redis-adapter');
const { createClient } = require('redis');
const pubClient = createClient({ url: 'redis://localhost:6379' });
const subClient = pubClient.duplicate();
io.adapter(createAdapter(pubClient, subClient));
// Now io.to('room1').emit(...) reaches sockets connected to ANY instance,
// not just the one that issued the broadcast.Load Balancer Configuration
Long-polling (the fallback transport) is a SEQUENCE of separate HTTP requests representing one logical connection. If a load balancer routes them to different backend instances, connection state breaks. Enable sticky sessions (session affinity) so every request from a given client consistently reaches the same instance — standard practice for any Socket.IO deployment behind a load balancer.
Validating Incoming Events
// A client can emit ANY event with ANY payload — treat it as untrusted
// input, exactly like a public REST endpoint body, not internal plumbing
socket.on('chat:message', (data) => {
if (typeof data?.text !== 'string' || data.text.length > 500) {
return socket.emit('error', 'invalid message');
}
const sanitized = sanitize(data.text);
io.to(data.room).emit('chat:message', { text: sanitized, from: socket.id });
});
// Offload heavy work — Node's single event loop means a blocking
// computation here stalls EVERY other connected socket's handling
socket.on('generate-report', async (params) => {
const jobId = await reportQueue.add(params); // don't compute inline
socket.emit('report-queued', { jobId });
});Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free