← Back to the library
System designApplied · 7 min

Real-time messages reach only some users after scaling out

“With one instance everything worked. After adding a second, chat messages, notifications, and presence updates reach some users and silently skip others, and which ones changes on every reconnect.”

THE PRINCIPLE

Connection state is local to one process; fan-out needs a shared backplane that every instance publishes to and subscribes from.

FIRST MOVES

  1. Confirm it's routing, not a bug: log the instance ID with every connect and every emit, then compare sender and recipient.
  2. Add a pub/sub backplane. In Socket.IO, swap the default in-memory adapter for a shared one (Redis, Redis Streams, Postgres, cloud pub/sub). With raw WebSockets, publish to Redis or NATS and have each instance deliver to its local sockets.
  3. Keep sticky sessions only where the transport needs them. Socket.IO's HTTP long-polling requires them; WebSocket-only transports don't.
  4. Address users and topics, not socket IDs: join rooms such as user:42 so any instance can target them.
  5. Know that plain Redis pub/sub is fire-and-forget. While a subscriber is disconnected, its messages are lost, so recover with replay from a stream or an HTTP snapshot.

TOOLS: THEN → NOW

Sticky sessions as the scaling fixSticky sessions for long-polling only, plus an adapter or broker for fan-out
Socket.IO Redis adapter on classic pub/subSharded adapter (Redis 7 sharded pub/sub), or the Redis Streams adapter when you need connection state recovery

PATTERN SNAPSHOT

import { createClient } from "redis";
import { Server } from "socket.io";
import { createShardedAdapter } from "@socket.io/redis-adapter";

const pubClient = createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);

const io = new Server({ adapter: createShardedAdapter(pubClient, subClient) });
// Any instance can now reach a user wherever they are connected.
io.to(`user:${userId}`).emit("notification", payload);

CLOSE THE AI. EXPLAIN THIS.

Why don't sticky sessions alone fix a broadcast that misses users on other instances?

HOW IT WORKS UNDERNEATH

SOURCES

Guide reviewed