Scaling Real Time

Scaling Real-Time Traffic Across Processes

Rooms live in the memory of one process. The moment you run two — cluster (Clustering), pm2 29,762 , containers behind a load balancer — a broadcast reaches only the clients connected to the process that sent it, and half your users see nothing. Two separate pieces fix that. An adapter relays broadcasts between processes: @socket.io/redis-adapter 8.3.0 over Redis 2,763 pub/sub with two ioredis 6.0.0 15,343 clients, @socket.io/redis-streams-adapter 0.3.1, @socket.io/postgres-adapter 0.5.0, or @socket.io/cluster-adapter 0.3.0 for workers on one host, which needs no extra service and is the one below. Sticky sessions keep a client's long-polling requests on the process that owns its session; without them a polling client gets Session ID unknown and reconnects forever. WebSocket-only clients do not need stickiness, but you cannot assume every client gets a WebSocket.

Two workers, sticky sessions, and one shared roomCSS
if (cluster.isPrimary) {
  const httpServer = createServer();
  setupMaster(httpServer, { loadBalancingMethod: 'least-connection' });
  setupPrimary();                                 // relays broadcasts between workers
  cluster.setupPrimary({ serialization: 'advanced' });   // required: sends binary over IPC
  httpServer.listen(4141);
  for (let i = 0; i < 2; i++) cluster.fork();
} else {
  const io = new Server(createServer(), { adapter: createAdapter() });
  setupWorker(io);                                // the primary hands this worker connections
  io.on('connection', (socket) => {
    socket.join('book:978-0441013593');
    console.log(`worker ${process.pid} accepted ${socket.id}`);
    socket.on('price', (p) =>
      io.to('book:978-0441013593').emit('price', { ...p, from: process.pid }));
  });
}

Two clients connect a second apart, landing on different workers; one then emits:

Output of 80
$ node cluster.js                              $ node cluster-client.js
worker 43488 accepted DLhDqvSEjJB7gVcVAAAA     a got $12.99 from worker 9600
worker 9600 accepted IaqVDaxvg4G_b2n2AAAA      b got $12.99 from worker 9600

Client a was on worker 43488 and still received what worker 9600 broadcast: the adapter carried it. Leave out cluster.setupPrimary({ serialization: 'advanced' }) and both clients fail with xhr poll error — a first-attempt mistake worth remembering. Across hosts, swap createAdapter() for the Redis adapter (Server-Side Caching with Redis) and configure stickiness at the proxy: nginx 75 ip_hash, or sticky cookies on a load balancer. Plan the restart too: a rolling deploy drops every socket at once, so raise reconnectionDelayMax on the client.