SYSTEM DESIGN · PATTERNS · 1 OF 7

Real-Time Updates: Pushing Changes to Millions of Open Screens

How to get a change from your servers onto a user screen within a second: the signals that call for push, why polling breaks, long polling vs SSE vs WebSockets, a separate connection tier, routing events with pub-sub or a connection registry, fan-out to huge audiences, catching up after a reconnect, and how to explain it all in an interview.

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
1 / 17

Press play to hear the narration, or step through at your own pace.

Notes

The signals

Listen for phrases like live, as it happens, or without refreshing. Chat, typing indicators, a courier on a map, live scores, stock prices, shared editing. In all of them the interesting direction is server to client, and the target is usually under a second. When you hear those, reach for this pattern.

Signals in the question
  • "see it live" / "as it happens"
  • chat and typing indicators
  • driver or courier location
  • live scores, comments, prices
  • collaborative editing
  • in-app notifications
  • latency target: under 1 s
  • data flows server → client

The naive design

The naive design is polling: every client asks every couple of seconds whether anything changed. With a million open screens that is half a million requests a second, and if only five thousand things actually change per second, ninety-nine percent of that work returns nothing. Worse, you are still up to two seconds stale. Polling faster just burns more servers.

Why short polling breaks
1M open screens
poll every 2 s
→ 500,000 requests/s

real changes: ~5,000/s
→ 99% of requests empty

and still up to 2 s stale
faster polling = more load

Move 1: long polling

The simplest upgrade is long polling. The client asks, and the server does not answer until there is something new or about thirty seconds pass. Then the client asks again straight away. Delivery is almost instant and it is plain HTTP, so it works through any proxy. The cost is a new request per event, and held requests that tie up server threads unless the server is asynchronous.

Long polling
  • client sends a request
  • server holds it open until
  • an event arrives or ~30 s pass
  • client immediately re-asks
  • + plain HTTP, works anywhere
  • + near-instant delivery
  • - one request per event
  • - held requests tie up workers

Move 2: SSE

Second move, Server-Sent Events. The client opens one HTTP request and the server keeps the response open, writing an event whenever something happens. It is one-way, server to client, which is exactly what a feed, a score or a notification badge needs. Browsers reconnect automatically and send the last event ID they saw, so the server can resume. Writes go up as ordinary POST requests.

Server-Sent Events
  • one long HTTP response,
  • events written as text lines
  • server → client only
  • + built into browsers
  • + auto-reconnect with
  • Last-Event-ID header
  • + works over HTTP/2
  • - no client → server channel
  • (send writes as normal POSTs)

Move 3: WebSockets

Third move, WebSockets. The connection starts as HTTP, upgrades, and becomes a two-way pipe with only a few bytes of overhead per message. Use it when the client also sends frequent small messages: chat, typing indicators, games, collaborative editing. The price is that you own reconnects, acknowledgements and heartbeats, and the connection is stateful, which makes load balancing harder. See Concepts episode 9.

WebSockets
  • HTTP upgrade, then a raw
  • two-way message channel
  • a few bytes of overhead each
  • + both directions, low latency
  • + ideal for chat, games,
  • collaborative editing
  • - you build reconnects, acks,
  • heartbeats yourself
  • - stateful: harder to balance

Choosing

How to choose. If a few updates a minute are fine, plain polling is honestly acceptable and cheapest to build. If updates flow one way, pick SSE. If the client chatters back constantly, pick WebSockets. Long polling is the fallback when a network blocks the others. Say it in that order, simplest first, and the interviewer sees judgement rather than buzzwords.

Pick the transport
need          poll  long  SSE  WS
updates/min    ok    ok    ok   ok
sub-second     no    ok    ok   ok
client sends   -     -     POST yes
often
through any    yes   yes   most most
proxy

default: SSE for one-way feeds,
WebSockets for two-way chatter

Connection tier

Now scale it. Open sockets are state, so put them on their own tier of connection servers that do nothing but hold connections and relay messages. Your app servers stay stateless and easy to deploy. A well-tuned box can hold a few hundred thousand idle connections, so ten million online users is roughly twenty of them. Use a layer four balancer with long idle timeouts, and send heartbeats so dead connections are noticed.

The connection tier
  • connection servers only hold
  • sockets and relay messages
  • app servers stay stateless
  • one tuned box: 100k to 1M
  • idle connections (memory-bound)
  • 10M online ÷ 500k ≈ 20 boxes
  • L4 balancer, long idle timeouts
  • heartbeat every ~30 s

Routing events

The hard question: user forty-two is connected to one of twenty boxes. Which one? The cleanest answer is pub-sub. When the user connects, that connection server subscribes to the channel user forty-two. The app publishes the event to that channel, and pub-sub delivers it to whichever server is subscribed. The app never needs to know where anyone is connected.

The alternative is a connection registry. Each connection server records user to server in Redis with a short expiry, refreshed by heartbeats. The app looks up the server and calls it directly. That avoids pub-sub traffic, but entries go stale after a crash, so every send needs a timeout and a fallback. Both are fine answers; name the trade-off.

Routing with pub-sub
  • user 42 connects to conn-7
  • conn-7 subscribes to
  • channel user:42
  • app publishes to user:42,
  • pub-sub delivers to conn-7,
  • conn-7 writes to the socket
  • app never needs to know
  • which box holds the user
Alternative: a registry
  • store user → server in Redis
  • set on connect, delete on
  • disconnect, short TTL
  • app looks up the server and
  • calls it directly
  • + no broadcast traffic
  • - stale entries after crashes
  • - extra lookup per message

Fan-out

Fan-out is the next trap. A goal in a final might go to two million screens. Never loop over two million users in the app. Publish once to the room's channel, let each connection server fan out to its own local viewers, and batch updates every couple of hundred milliseconds. For floods like live comments, sample them. Nobody can read five thousand comments a second anyway.

Fan-out to huge rooms
  • one goal → 2M open screens
  • publish once per room, not
  • once per viewer
  • each conn server fans out to
  • its own local viewers
  • batch: send every 200 ms
  • sample: show 1 in 50 comments
  • drop old updates for slow users

Reconnects

Phones lose signal all the time, so treat the push as a fast hint and the durable store as the truth. Write each event with a sequence number before pushing it. The client remembers the last number it saw. On reconnect it says, give me everything after one hundred eighteen, and the server replays the gap. That gives at-least-once delivery; the client drops duplicates by event ID.

Never lose an update
  • every event gets a sequence
  • number, written to a durable
  • store before it is pushed
  • client remembers last seq
  • on reconnect: "since 118"
  • server replays 119 to 124
  • delivery: at least once
  • client dedupes by event id

Failure modes

The failure modes interviewers probe. Deploying a connection server drops half a million sockets, so drain them gradually. After an outage every client reconnects at once, so use exponential backoff with random jitter. A slow phone on a bad network can fill your send buffer, so cap it and skip stale updates. And heartbeats catch zombie connections that never closed cleanly.

Failure modes
  • deploy kills 500k sockets
  • → drain slowly, tell clients
  • to reconnect elsewhere
  • reconnect storm after outage
  • → random jitter + backoff
  • slow client fills a buffer
  • → cap buffer, drop or skip
  • zombie sockets
  • → heartbeats + idle timeout

Trade-offs

The trade-offs to say out loud. Push gives freshness but costs stateful infrastructure that polling does not need. SSE is simpler, WebSockets are more capable. Pub-sub routing spends broadcast traffic, a registry risks stale entries. You promise at-least-once delivery with deduplication, not exactly once, and ordering per conversation or per document, not across the whole system.

Trade-offs
  • push vs poll: freshness vs
  • simplicity and cost
  • SSE vs WebSocket: one-way
  • simplicity vs two-way power
  • pub-sub vs registry: fan-out
  • traffic vs stale lookups
  • exactly once? no: at least
  • once plus dedupe
  • ordering: per conversation,
  • not global

Where it shows up

Where you will use this: WhatsApp and Slack for messages and presence, live comments for huge fan-out, Google Docs for edits in both directions, Uber and food delivery for moving dots on a map, a stock exchange for price ticks, a leaderboard for rank changes, and the notification system for unread badges. Each of those has its own problem episode.

Problems that use it
  • WhatsApp, Slack: chat, presence
  • Live comments: huge fan-out
  • Google Docs: edits both ways
  • Uber, food delivery: location
  • Stock exchange: price ticks
  • Leaderboard: rank changes
  • Notification system: badges
  • Zoom: signalling channel

Say it like this

Here is how to say it. Clients keep a WebSocket, or SSE if it is one-way, to a dedicated connection tier behind a layer four balancer. The app publishes events to a per-user or per-room channel, and the server holding that socket pushes it. Events are stored with sequence numbers so a reconnecting client catches up. Big rooms fan out per server and batch.

In the interview
  • "Clients keep a WebSocket to
  • a connection tier behind an
  • L4 balancer. The app publishes
  • to a per-user channel; the
  • server holding that socket
  • pushes it. Events are stored
  • with sequence numbers so a
  • reconnecting client catches
  • up. Big rooms fan out per
  • server and batch."

Interview questions

When would you choose SSE over WebSockets?

When updates flow only from server to client, such as feeds, scores or notification counts. SSE runs over ordinary HTTP, reconnects automatically with Last-Event-ID, and client writes can go up as normal POST requests.

Why not just poll every second?

At a million open clients that is hundreds of thousands of requests per second, most returning nothing, and data is still up to a second stale. Push sends work only when something actually changes.

How do you know which server holds a given user connection?

Either each connection server subscribes to a pub-sub channel per connected user, so publishers never need to know the location, or you keep a user-to-server registry in Redis with a TTL refreshed by heartbeats and send directly.

How many WebSocket connections can one server hold?

Idle connections are mostly memory, so a tuned server commonly holds a few hundred thousand and up to around a million; the real limits are memory per connection, file descriptors and how many messages per second you push.

How do you avoid losing messages when a client disconnects?

Persist every event with a per-stream sequence number before pushing it. The client sends its last seen sequence on reconnect and the server replays the gap, giving at-least-once delivery with client-side deduplication.

How do you push one event to two million viewers?

Publish once to the room channel and let each connection server fan out to its own local subscribers, batching updates every few hundred milliseconds and sampling or dropping low-value updates for slow clients.

What happens when you deploy a connection server?

All its sockets drop, so you drain it gradually, signal clients to reconnect, and rely on client backoff with jitter so the reconnections spread across the remaining servers instead of arriving as a storm.

Why separate the connection tier from the application servers?

Connections are long-lived state, while business logic should be stateless and easy to deploy and autoscale. Splitting them lets you restart app servers without dropping millions of sockets and scale each tier on its own bottleneck.