Skip to content

Scaling WebSockets and real-time fan-out

A socket belongs to one pod, so every broadcast crosses a backplane, every reconnect needs a resume point, and every deploy moves thousands of sockets.

Predict first

Before you read: predict

ExamHall runs its proctor WebSocket service on three pods, A, B and C. Each pod keeps a rooms map from an exam room to WebSocket connections in that room. So that an alert reaches the room's consoles on every pod, each pod runs:

The exam:ECON-201 room has 2 proctor consoles on pod A, 2 on pod B and 2 on pod C. Pod C raises one alert for that room.

How many copies of the alert reach consoles in total?

Code for this question
sub.subscribe('alerts', (raw) => {               // sub: this pod's Redis subscriber  const alert = JSON.parse(raw);  for (const ws of rooms.get(alert.room) ?? []) ws.send(raw);}); function raiseAlert(alert) {                     // runs on the pod whose anti-cheat check fired  const raw = JSON.stringify(alert);  for (const ws of rooms.get(alert.room) ?? []) ws.send(raw);  pub.publish('alerts', raw);                    // pub: this pod's Redis publisher}
Pick one

The 30-second version

A WebSocket stays on the pod that accepted it, and only that pod can write to it. To reach a room spread across pods, pods relay through pub/sub: every pod, the publisher included, receives each message and sends it to its own members. Number each room's messages, buffer the newest, cap slow sockets, and drain on a schedule.

A socket belongs to one pod, so every broadcast crosses a backplane, every reconnect needs a resume point, and every deploy moves thousands of sockets.

How it works

Step 1 of 14

Pinned. A WebSocket is one long-lived TCP connection. It stays on the pod that accepted it, and only that pod can write to it.
In the kitchen
  • a circular handed in at the east office reaches only east-round addresses → a broadcast reaches one pod's members (steps 1–2)

Analogy: A town's postal delivery offices

Every address belongs to one office's round. The offices swap copies on the night van, and each office delivers what the van brings to its own rounds.

At the delivery offices In the socket fleet
a circular handed in at the east office reaches only east-round addresses a broadcast reaches one pod's members (steps 1–2)
the night van carries every office's copies to every office the pub/sub backplane (steps 3, 5)
an office that delivers its own copy and also takes it back off the van the publisher's own echo (step 4)
each club numbers its circulars; the office keeps the last few on a shelf per-room numbers and a bounded buffer (steps 7–9)
a jammed letterbox: post piles up at the office until a "collect from us" card a slow reader, capped and closed (steps 10–11)
closing an office: its streets go to neighbours a few at a time draining at a rate (steps 12–13)

Breaks down when: a copy of a message costs nothing to make, so the danger is duplicates and memory, not lost paper; and an office knows its streets in advance, while a pod learns its rooms only as sockets join.

Contrasting case: a parcel locker keeps a delivery until it is collected. Pub/sub keeps nothing, so the replay buffer is something you build.

In real terms: delivery follows the connection to the one pod that holds it, and everything else (relay, numbering, buffering, draining) exists because of that.

Examples, run for real

4.1 ExamHall EXH-2340: the alert reached one proctor of five

Exam alerts are emitted with Socket.IO's io.to(room).emit(). Moved from one server to three, with the room's five consoles spread over them, an alert raised on B reached only B's console. MR !2640 adds the Redis adapter, a publisher and a subscriber connection per server:

4.1 Socket.IO 4.8.4 · 3 servers · room exam:ECON-201: A 2, B 1, C 2 consoles · io.to(room).emit() on B
4.1 default in-memory adapter: consoles reached 1 of 5 (A1:0 A2:0 B1:1 C1:0 C2:0)
4.1 @socket.io/redis-adapter 8.3.0, redis 7.4.9: consoles reached 5 of 5 (A1:1 A2:1 B1:1 C1:1 C2:1)

With the adapter, B sends to its own console and publishes; A and C send to theirs (steps 2–4). Each console got one copy.

4.2 Media streaming: two pods number one chat room

A live stream's chat room has viewers on two pods, and both pods raise messages into it.

4.2 two pods raise 6 chat messages into room stream:final, alternately
4.2 a counter in each pod:   numbers 1 1 2 2 3 3 · repeated numbers 3
4.2 one Redis INCR per room: numbers 1 2 3 4 5 6 · repeated numbers 0

With a counter per pod, "the last message I saw was 2" names two messages, so no resume point is safe (steps 7–8).

Try it

Answer first, then check. Nothing is saved.

Question 1 of 5

A streaming service shows a live chat beside a cup final. Every viewer keeps a WebSocket open for the whole match and types about one message a minute; the moderators release 5 messages a second, and each released message is shown to every viewer. One socket pod can give 2 GiB of memory to connections, at 40 KiB per open WebSocket, and can write 150,000 WebSocket messages a second. Pods relay released messages to each other through a Redis backplane.

How many viewers can one pod carry?

Pick one

Question 2 of 5

ExamHall's alert service numbers each exam room's alerts from 1 and keeps each room's newest 50 in Redis. A console that reconnects can catch up: it sends, for each room it watches, the number of the last alert it received, and the service answers each room by its resume rule. A chief invigilator's console watches three halls. Before its connection drops, it has received alert #180 in Hall 1, #42 in Hall 2 and #13 in Hall 3. While it is away, Hall 1 reaches #230, Hall 2 #49 and Hall 3 #64.

Which resume rule replays as many alerts as it can, and never leaves the console showing a room with an alert missing?

Pick one

Question 3 of 5

A live match-stats service pushes a 64 KiB frame to each of three viewers for every update from its stats feed, and each frame carries only what changed since the one before. One viewer's phone app freezes: its connection stays open, but it reads nothing. The service's loop is for (const ws of viewers) ws.send(frame); on ws 8.22.0, and the feed now sends a burst of 400 updates.

Which change keeps what the server holds for any one viewer under 2 MiB, while every viewer that keeps its connection open gets every frame in order?

Pick one

Question 4 of 5

During a deploy, ExamHall's platform will send one socket pod a stop signal with 90 minutes of the exam still to run. The pod holds 12,000 console connections. A console whose connection closes reconnects to another pod after a random wait of 0 to 4 s. Every new connection's token is checked by the auth service, which admits at most 1,000 new connections a second and refuses the rest. The platform kills the pod 60 s after the stop signal.

Which plan keeps every second at or under 1,000 new handshakes?

Pick one

Question 5 of 5

A support desk runs 120 live conversations, each between one customer and one agent on two different socket pods; every pod holds the same number of people. Each conversation produces one message a second. Today every message goes through one Redis channel, which all pods listen on; each pod passes a message to its own people in that conversation. The fleet is growing from 4 pods to 8.

Which design makes each pod's listener take in fewer messages a second at 8 pods than at 4, and everyone still gets each message of their own conversation exactly once?

Pick one

Go deeper

Pro tips
  1. After every deploy or scale-out, chart open connections per pod beside requests per second; a new pod starts empty and stays light until clients reconnect. When: a fleet that looks balanced on requests a second is not balanced on sockets, the curriculum's "like HTTP" misconception (step 1). Verify: tip server D started: open connections per server A 2 · B 1 · C 2 · D 0 Status: RAN.
Source

Socket.IO, Redis, MDN and Kubernetes behaviour can change: the facts below are as of 2026-10-01. No item is keyed on a source; every key is executed. Valid as of 2026-10-01.

  • IETF, RFC 6455, The WebSocket Protocol: https://www.rfc-editor.org/rfc/rfc6455.txt (rfc6455.txt). The curriculum row's first source. Quoted: close code "1001 indicates that an endpoint is "going away", such as a server going down".
  • Socket.IO docs, Redis adapter: https://socket.io/docs/v4/redis-adapter/ (socketio-redis-adapter.txt). Quoted, as of 2026-10-01: every packet sent to multiple clients is "sent to all matching clients connected to the current server" and "published in a Redis channel, and received by the other Socket.IO servers of the cluster"; the sharded adapter's dynamic mode is "Useful when some rooms have a low number of clients (so only a few Socket.IO servers are notified)"; and "the Redis adapter uses the Pub/Sub mechanism to forward the packets between the Socket.IO servers, so there are no keys stored in Redis".
  • Socket.IO docs, Using multiple nodes: https://socket.io/docs/v4/using-multiple-nodes/ (socketio-multiple-nodes.txt). Quoted, as of 2026-10-01: "WebSocket establishes a single, long-lived TCP connection between the client and server" and "The client maintains its connection to the same server for the entire session duration". Supports step 1.
  • Redis docs, Redis Pub/sub: https://redis.io/docs/latest/develop/pubsub/ (redis-pubsub.txt; the row's develop/interact/pubsub path now lives here). Quoted: messages "will be pushed by Redis to all the subscribed clients", "Subscribers receive the messages in the order that the messages are published", and "Redis' Pub/Sub exhibits at-most-once message delivery semantics"; in sharded Pub/Sub, "shard channels are assigned to slots by the same algorithm used to assign keys to slots". Supports steps 3–6 and every run's flush barrier.
  • Redis docs, INCR: https://redis.io/docs/latest/commands/incr/ (redis-incr.txt).
  • MDN, WebSockets API: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API (mdn-websockets-api.txt), and WebSocket: bufferedAmount: https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/bufferedAmount (mdn-bufferedamount.txt). Quoted: bufferedAmount "returns the number of bytes of data that have been queued using calls to send() but not yet transmitted to the network", and "if you keep calling send(), this will continue to climb". Supports step 10.
  • ws 8.22.0, API documentation: https://raw.githubusercontent.com/websockets/ws/8.22.0/doc/ws.md (ws-doc.txt). Quoted: websocket.bufferedAmount is "The number of bytes of data that have been queued using calls to send() but not yet transmitted to the network".
  • Kubernetes docs, Pod Lifecycle, termination of Pods: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/ (k8s-pod-lifecycle.txt). Quoted, as of 2026-10-01: a Pod "is granted a term to terminate gracefully, which defaults to 30 seconds", and "Once the grace period has expired, the KILL signal is sent to any remaining processes".

Checked against the tools and docs as of 2026-10-01.