Lesson 9 / 25

Scaling WebSockets and Server-Sent Events

Fan out live updates across many WebSocket servers with Redis.

A backplane for real-time servers

A single WebSocket or SSE server can push messages only to users connected to it. With many servers behind a load balancer, a message for user 42 must reach whichever server holds user 42's connection. Redis is a common backplane: every real-time server subscribes to relevant channels (per user, per room or per topic), and any service publishes updates to Redis, which fans them out to all servers; each server forwards to its own connected clients. Frameworks build on this: the Socket.IO Redis adapter (including a sharded variant for Redis 7), SignalR's Redis backplane in ASP.NET Core, and Spring with Redis message listeners. Because Pub/Sub is fire-and-forget, design clients to resynchronise after reconnecting, for example by fetching missed notifications from an API, or by reading recent entries from a Redis Stream that also stores the updates.

A Node.js WebSocket server using Redis as a backplane

Each server subscribes once and forwards messages to its local sockets for that user.

import { createClient } from 'redis';
import { WebSocketServer } from 'ws';

const sub = createClient({ url: process.env.REDIS_URL });
await sub.connect();

const wss = new WebSocketServer({ port: 8080 });
const socketsByUser = new Map();           // userId -> Set of sockets on THIS server

wss.on('connection', (ws, req) => {
  const userId = authenticate(req);        // e.g. from a token
  if (!socketsByUser.has(userId)) socketsByUser.set(userId, new Set());
  socketsByUser.get(userId).add(ws);
  ws.on('close', () => socketsByUser.get(userId)?.delete(ws));
});

await sub.pSubscribe('notify:user:*', (message, channel) => {
  const userId = channel.split(':')[2];
  for (const ws of socketsByUser.get(userId) ?? []) ws.send(message);
});

// any service: await redis.publish(`notify:user:${userId}`, JSON.stringify(payload));

Switchboard operators in different buildings

Each building has an operator connected to its own phones. A central announcement system (Redis) tells every operator about a call for extension 42; only the operator who has extension 42 actually rings it.

Quick check: Why do multiple WebSocket servers need a backplane such as Redis Pub/Sub?

  • WebSockets cannot send JSON
  • Redis encrypts WebSocket traffic
  • A message must reach whichever server holds the target user's connection
  • Load balancers cannot route WebSockets
Answer

A message must reach whichever server holds the target user's connection — The backplane fans messages out so the server holding the right connection can deliver them.