Real-time Updates
How to push data from server to client, how to route it to the right server, and how to pick the simplest option that still meets the requirement.
Contents
Real-time updates deliver data changes from servers to clients the moment they happen — chat messages, driver notifications, live comments, collaborative edits. A lot of candidates bungle this, not because they’re bad engineers, but because real-time systems touch networking concepts many of us never had to build from scratch. The trick that makes it manageable: break it into two independent hops.
01The problem, and the two hops
Consider a collaborative editor like Google Docs. When one user types a character, every other viewer needs to see it within milliseconds — not 3–5 seconds later. The naive fix, every user constantly polling “any changes now? now? now?”, would crush your infrastructure with millions of requests every few milliseconds.
The root difficulty: standard HTTP is request-response. Clients ask, servers answer, the connection closes (or at best stays open waiting for another client-initiated request). That model isn’t built to let a server push updates on its own.
Think about how data flows through the system, and real-time updates split into two distinct problems:
Hop 1 — server → client: how do I get an update from a server out to the connected client? This is a networking / protocol question.
Hop 2 — source → server: how does an update reach the specific server that the target client is connected to? This is tricky because the server connected to your client usually isn’t the one that generated the update.
These problems are often solved once by a specialized infra team, so plenty of engineers with 10+ years of experience have never built them. The interviewer’s baseline expectation here is usually low — the goal is to know the options and justify your choice, not to have built it before.
02Networking 101
In some sense these are just networking problems. Networks use a layered architecture (the OSI model) where each layer builds on the abstraction below it. Three layers matter most in interviews:
| Layer | Protocols | What it gives you |
|---|---|---|
| L3 · Network | IP | Routing/addressing, breaks data into packets, best-effort delivery. No guarantees — packets can be lost, duplicated, or reordered. |
| L4 · Transport | TCP, UDP | TCP is connection-oriented: set up a connection first, then get correct, in-order delivery (costs time, resources, bandwidth). UDP is connectionless: send to any IP with no setup, no ordering/delivery guarantee — “spray and pray.” |
| L7 · Application | DNS, HTTP, WebSockets, WebRTC | Application protocols built on top of TCP, abstracting data types for web apps. |
The request lifecycle — and its hidden overhead
Typing a URL kicks off a carefully orchestrated sequence. First DNS resolves the domain to an IP, then:
- DNS resolution — domain → IP (one round trip; usually cached, but still overhead).
- TCP handshake (3-way) —
SYN→SYN-ACK→ACKestablishes the connection. - HTTP request — the client sends its actual GET.
- Server processing — checks a DB, prepares a response.
- HTTP response — the data comes back.
- TCP teardown (4-way) —
FIN→ACK→FIN→ACKcloses the connection.
For every single “has anyone typed anything new?” request, you’re potentially paying DNS resolution + a 3-way handshake + a 4-way teardown — and 90% of the time the answer is “nope, nothing new.” Multiply that overhead by millions of users and naive polling clearly won’t work. Seeing this overhead concretely is what makes the smarter protocols make sense.
1. Latency: every round trip (including connection setup) adds latency before your application data even moves.
2. State: a TCP connection is state both sides must maintain. Without HTTP keep-alive, you repeat this setup for every request — significant overhead.
03L4 vs L7 load balancers
Real systems put multiple servers behind a load balancer. The L4-vs-L7 distinction returns directly when we choose real-time protocols.
Layer 4 (transport)
- Routes on IP/port; doesn’t inspect packet content.
- Maintains a persistent TCP connection between client and server — as if the client has a direct TCP line to one backend.
- Fast, minimal inspection; can’t route on app data.
- Well-suited to persistent connections like WebSockets.
Layer 7 (application)
- Understands HTTP; can route on URL, headers, cookies.
- Terminates incoming connections and opens new ones to backends.
- More CPU-intensive; more flexible/featureful.
- Better for HTTP-based solutions like long polling.
Some L7 balancers explicitly support connection-oriented protocols like WebSockets, but generally L4 is better for WebSockets while L7 offers more flexibility for HTTP-based approaches. An L7 balancer doesn’t guarantee the same TCP connection per request — which is exactly the guarantee persistent protocols need.
04Hop 1a · Simple polling — the baseline
The simplest approach: the client makes an HTTP request at a regular interval and the server responds with the current state. For a chat app, you constantly poll “what messages haven’t I received yet?”. It doesn’t technically qualify as real-time, but it’s the right starting point far more often than candidates think.
async function poll() { const response = await fetch('/api/updates'); const data = await response.json(); processData(data); } // Poll every 2 seconds setInterval(poll, 2000);
Too many candidates jump straight to WebSockets or complex pub/sub when polling every 2–5 seconds would work fine. Most interview problems don’t need sub-second updates — a stock price every 3 seconds or a dashboard every 5 is totally acceptable. Always start by asking “how real-time does this actually need to be?” — you can even ask the interviewer. If the answer is “pretty responsive,” polling is usually adequate. Save the complex stuff for genuine sub-second needs.
Give yourself a backdoor: “I’ll start with a simple polling approach so I can focus on X, but we can switch to something more sophisticated if we need to.” But do propose it before committing — if the interviewer is dead-set on WebSockets/SSE/WebRTC, you want to know early. To counter the “too slow / inefficient” objection, mention HTTP keep-alive set longer than the polling interval, so you reuse one TCP connection instead of re-establishing it each time.
Good for: anything not requiring very-low-latency updates, and short update windows (e.g. the LeetCode design). Costs: higher latency (up to interval + processing), limited frequency, more bandwidth, and connection churn with many clients.
05Hop 1b · Long polling — the easy upgrade
Long polling: instead of the server immediately replying “no updates,” it holds the request open until data is available or a timeout occurs. It’s as if the server is just taking a long time to respond. Then it answers, the client immediately opens a new request, and the cycle repeats.
// Client-side of long polling async function longPoll() { while (true) { try { const response = await fetch('/api/updates'); const data = await response.json(); processData(data); } catch (error) { console.error(error); await new Promise(r => setTimeout(r, 1000)); // back off on error } } }
This cuts the wasteful “no updates” replies. But it has two costs. First, the held-open request is effectively state — and we generally want to minimize state. Second, it introduces extra latency for high-frequency updates, because each update needs the client to “call back” before the next one can be sent.
The webpage’s worked example (assume 100ms client↔server latency):
The video frames the same idea: for the second update you “ate about a round trip of latency” because you had to wait for the client to call back.
Long polling is a great, low-complexity near-real-time solution, but not suitable for high-frequency updates (many updates near each other). Also: browsers limit concurrent connections per domain; monitoring is painful since requests hang around; and each infra hop must tolerate the long request — don’t let a load balancer hang up at 30s when your server holds for 60s (15–30s is a common interval that minimizes fuss).
A long async process where you want to know the instant it finishes — like payment processing: long-poll for the payment status, then show the success page.
06Hop 1c · Server-Sent Events — the efficient one-way street
SSE extends long polling so the server can send a stream of data. Instead of a fixed Content-Length, it uses Transfer-Encoding: chunked to tell the client the response is an open-ended series of chunks. This turns one atomic request/response into a granular stream of events.
With long polling, every request needed a single response, so you waited for the client to call back between updates. With SSE you can send multiple responses down the same HTTP response — each event pops up client-side like a push notification, with no call-back gap. The browser side is the built-in EventSource object, which also handles automatic reconnection.
// Client-side const eventSource = new EventSource('/api/updates'); eventSource.onmessage = (event) => { const data = JSON.parse(event.data); updateUI(data); }; // Server-side (Node/Express) app.get('/api/updates', (req, res) => { res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); const sendUpdate = (data) => res.write(`data: ${JSON.stringify(data)}\n\n`); dataSource.on('update', sendUpdate); req.on('close', () => dataSource.off('update', sendUpdate)); });
The key headers: Content-Type: text/event-stream, no caching (it’s real-time, theoretically uncacheable), and connection kept alive. A very popular modern use case is AI chat apps streaming tokens as they’re generated.
One-way only (server→client). Some proxies and load balancers don’t support streaming — they buffer the response until it completes, silently blocking your stream in a way that’s nasty and opaque to debug. You must ensure your load balancer and API gateways won’t close long-open connections. Browsers also cap concurrent connections per domain.
SSE connections aren’t super long-lived (30–60s typical). The SSE standard includes a “last event ID” the browser sends on reconnect, so the server can replay everything the client missed while disconnected. Most interviewers aren’t familiar with SSE’s infra details and won’t grill you — unless the role is very frontend-centric.
07Hop 1d · WebSockets — the full-duplex champion
WebSockets are essentially an abstraction over a TCP channel: bytes flow in either direction with no request/response pairing. They build on HTTP via an “upgrade” that switches an existing TCP connection’s L7 protocol (so you can reuse cookies/headers). You get TCP’s guarantees — ordering, delivery acknowledgement — and build your own message protocol on top.
Because it’s a raw channel, in an interview you define your API in terms of the messages you send. Some have responses, some don’t — e.g. the server sends ticker update messages while the client sends subscribe. It probably makes sense for the server to ack a subscription, but maybe not — your choice depends on the use case.
// Client-side const ws = new WebSocket('ws://api.example.com/socket'); ws.onmessage = (event) => handleUpdate(JSON.parse(event.data)); ws.onclose = () => setTimeout(connectWebSocket, 1000); // reconnect
Use an L4 load balancer. A WebSocket is basically a bare TCP connection; you can’t redirect WebSocket messages to different services the way an L7 balancer redirects HTTP. (L7 balancers that support WebSockets exist — fine to call out — but interviewers aren’t unusually sensitive to the choice.)
Terminate WebSockets early — usually into a dedicated WebSocket service at the front of your infra. That way not every backend service needs the same stateful connections, and the rest of the system stays stateless. A driver/rider service that everyone connects to also makes state easier to manage.
Unlike SSE (which reconnects every ~30s), a WebSocket may stay open for hours, so the infra must support it end-to-end. Deployments are a problem: on redeploy you either sever old connections and let clients reconnect (simpler, preferred) or migrate them. Handle missed updates on reconnect. Load balancing is harder because you must “stick with” each allocation — a “least connections” strategy helps avoid hotspots.
Candidates adopt WebSockets too eagerly. Since you can always make separate requests for writes, a common pattern is SSE subscriptions for updates + simple HTTP POST/PUT for writes. Defer to SSE unless you have a specific need for bidirectional communication.
08Hop 1e · WebRTC — the peer-to-peer solution
WebRTC enables direct peer-to-peer connections between clients — great for video/audio calls and some data sharing. It sounds odd to discuss on the backend, but setting up a P2P connection involves real backend responsibilities.
How the setup works
- Signaling server — clients connect here to discover peers and exchange connection info (ICE candidates). It’s lightweight, handles little bandwidth, and is only used in the initial handshake — so it needn’t be heavily latency-optimized. It can be regionalized/partitioned (e.g. never connect North America to Europe) or global.
- STUN (“Session Traversal Utilities for NAT”) — most clients don’t accept inbound connections for security, sitting behind NAT. STUN uses techniques like “hole punching” to establish a publicly routable address/port, shared via the signaling server.
- Direct connection — client A connects to client B over those public ports.
- TURN — fallback when STUN fails (e.g. behind a corporate firewall): relay traffic through a central server. It’s the least desirable mode because all the bandwidth now flows through your servers — the very thing P2P was avoiding.
For most “peer-to-peer looking” scenarios, a WebSocket is preferable: you keep server-side control and dodge STUN/TURN servers and malicious clients connecting to each other. A server in the middle usually isn’t a big deal — except for the latency and bandwidth it costs. That exception is exactly where WebRTC wins: video conferencing (Google Meet) and multiplayer gaming, where a relay’s added latency and bandwidth would be prohibitively expensive.
The signaling server itself is effectively a real-time update system (peers must find each other and update connection info), so it needs WebSockets/SSE under the hood. WebRTC can also reduce server load when clients talk to each other frequently — Canva used it for presence/pointer sharing in its canvas editor, and it pairs well with CRDTs for collaborative editing.
09Hop 1 · The decision flow
WebSockets are kind of a last resort. Despite everyone wanting to talk about them, you’ll usually find polling or SSE more appropriate — and simpler. Be careful about prematurely pulling the trigger on stateful real-time communication when it isn’t needed.
10Hop 2 · Getting updates to the right server
With persistent connections (long-polling, SSE, WebSockets), each client is connected to one server, and that server sends its updates. The problem: to send a message to User C, we must know which server User C is connected to. Three patterns.
Pattern A · Pulling with simple polling
A pull model: the client polls, and the server queries a database for what’s new (e.g. “messages newer than my last one?”). This neatly sidesteps the routing problem — a server only pulls updates for the clients connected to it — and decouples the update source from the receiver via the DB.
You can also have the server poll a DB at a higher frequency than a client could (it’s cheaper — inside the data center). But in interviews, server-side polling is less common and usually a hack/kludge when you’re short on time. If the interviewer set up a problem that needs real-time updates, they usually want the full near-zero-latency setup, not a faster poll. And watch the scale: 1M clients polling every 10s = 100k TPS of read volume.
Pattern B · Pushing via consistent hashing
Assign each user to a specific server so we always know where they’re connected. Simple modulo hashing (user % N) works when N is fixed but forces almost everyone to reconnect when you scale. Consistent hashing maps servers and users onto a ring; each user connects to the next server clockwise, so scaling only moves the users in affected ring segments.
You need (1) a place to store and update the ring parameters — the video likes ZooKeeper, whose znodes are a lightweight way for servers to learn about each other; the ring is just an ordered server list + an agreed hash function, so each server can compute routing itself. (2) A way for users to reach the right server: either a middle server that stays subscribed to ZooKeeper, hashes the incoming user ID, and returns the right server; or the Redis-style approach where users randomly ping a server that either accepts them or redirects (“you actually belong to server two”). Both are fine.
Adding/removing nodes is a delicate balance. During a transition, servers must know both the old and new assignments until it completes, and messages may be sent to both the old and new server in the interim. Avoid a thundering herd: if you dump all users off a server at once (or take it down mid-move), you cause even more churn — and new connections are resource-heavy to spin up. Instead, formally signal the scaling event and drain connections slowly, giving the new server time to pick them up (back-pressure/feedback makes this more robust). Also: connection state is lost if a server fails. Because these deep dives open up, consider simplifying to Pub/Sub if real-time isn’t the core of the problem.
Pattern C · Pushing via Pub/Sub
A single service collects updates and broadcasts them to interested clients. The Pub/Sub service becomes the main source of state; your persistent connections now terminate at lightweight endpoint servers that just subscribe to topics and forward to clients.
Connect: client connects to any endpoint server (a plain load balancer, even random allocation, is fine). That server registers a subscription with the Pub/Sub service for the user’s topic.
Send: the update source publishes to the topic → subscribed endpoint servers receive it → each looks up its local WebSocket connection for that user and forwards the message. The Pub/Sub does the hard part; endpoint servers are dumb proxies.
Redis Pub/Sub — lightweight, in-memory, no durability. Perfect “fire-and-forget” for something like a Yahoo-Finance stock ticker where you only care about the latest value (the full archive lives elsewhere).
Kafka — heavier, but full replay logs. When it’s important that all updates eventually reach users, store messages in the topic and replay what a reconnecting client missed. You don’t have to use Kafka for durability, but it makes life easier. (See the WhatsApp guide: Pub/Sub for low-latency chat delivery + a DB for message storage, doing both.)
You don’t know whether subscribers are still connected (or when they drop). The Pub/Sub service is a single point of failure and bottleneck, adds a layer of indirection/latency (still typically <10ms), and creates many-to-many connections between Pub/Sub hosts and endpoint servers. Scale it with a Redis cluster (shard subscriptions by key); use least-connections load balancing for the endpoint servers.
Ask “how stateful are my services?” If you already need heavy per-connection state (e.g. Google Docs: a document’s collaborative-editing state), consistent hashing keeps that state in one place. If you’re just passing small messages, Pub/Sub keeps endpoint servers lightweight and interchangeable — and it’s the better default for most problems.
11Putting the two hops together
| Application | Hop 1 (server→client) | Hop 2 (source→server) | Why |
|---|---|---|---|
| Live dashboard | Polling (if ≥5s OK) — or SSE | None if polling — else Pub/Sub | Push ticker updates; broadcast to all users subscribed to that ticker. Cheap and easy. |
| Collaborative editor | WebSockets | Consistent hashing | Bidirectional + high-frequency. Put all (max ~100) users of a doc on the same server → simple broadcast, no extra hops, and it’s where CRDTs/operational transforms run. |
| Chat app | SSE for receiving + REST for sends | Pub/Sub (or consistent hashing) | Bidirectional but not high-frequency; sends are relatively rare, so a REST call per send is fine. |
For chat, beware a room that becomes huge. With consistent hashing, millions of users in one room all land on one server and overload it — the opposite of what you want. That pushes you toward Pub/Sub. Seeing this trade-off play out is the point: justify your choice per the workload.
Real-time needs appear in almost every interactive design. Don’t wait to be asked — flag it early: “messages need to be delivered instantly, I’ll use WebSockets,” or “character-level changes need sub-second propagation.” And know when NOT to: if you can tolerate polling, you avoid both hops — a simplicity highly valued at senior+.
12Common deep dives
Connection failures & reconnection
Networks are unreliable — mobile drops, WiFi cuts out, servers restart. The challenge is detecting disconnects fast and resuming without data loss. WebSocket breaks don’t always signal cleanly: a client may think it’s connected while the server already cleaned up (“zombie” connections). Use heartbeats to detect them. For recovery, track what each client has received (per-user message queue or sequence numbers) and replay the gap on reconnect — Redis streams are popular here.
The celebrity problem (millions of followers)
When a celebrity posts, millions need the update at once; naive fan-out crashes the system. The fix is strategic caching + hierarchical distribution: cache the update once and distribute through layers — regional servers pull it and push to their local clients — instead of writing to millions of individual feeds.
Message ordering across servers
Two messages sent milliseconds apart can arrive out of order across different paths/servers. Vector clocks or logical timestamps establish ordering relationships. For strict ordering, funnel related messages through a single server or partition — trading some scalability for a simple total order.
Prefer the single server/partition for ordering. Vector clocks matter for deep-infra questions, not for “Design an Online Auction.” If all messages hit one host, stamping a correct timestamp and establishing total order is straightforward.
13Active recall
What are the “two hops,” and why solve them separately?
Hop 1 is server→client (a protocol/networking choice: polling, long polling, SSE, WebSockets, WebRTC). Hop 2 is source→server — routing an update to the specific server the target client is connected to (pull-polling, consistent hashing, or Pub/Sub). They’re a natural partition with different trade-offs, so you pick each independently, then combine.
Why is naive polling so wasteful — and when is it still the right call?
Each request can pay DNS + a 3-way TCP handshake + a 4-way teardown, and ~90% of the time the answer is “nothing new,” so you burn overhead for nothing (×millions of users). Yet it’s still the right default when you don’t need sub-second updates: it’s simple, stateless, needs no special infra, and is fast to explain. Use HTTP keep-alive longer than the interval to cut setup cost.
Why does long polling add latency for the second of two close updates?
Each request yields a single response, so after update 1 returns, the client must open a new request before update 2 can be sent. At 100ms latency, update 1 arrives ~100ms after it happened, but update 2 can be ~290ms late (90ms to finish response 1 + 100ms for the next request + 100ms for its response). That’s why long polling is unsuitable for high-frequency updates.
How does SSE fix long polling’s frequency problem, and what’s its main limitation?
SSE uses Transfer-Encoding: chunked to send many events down one open HTTP response, so there’s no client call-back gap between updates. Limits: it’s one-way (server→client) and some proxies/load balancers buffer streaming responses, silently breaking it. Reconnection uses a “last event ID” to replay missed events.
When do you choose WebSockets over SSE, and what infra do they demand?
Choose WebSockets for frequent bidirectional communication; otherwise prefer SSE (a common pattern is SSE for updates + HTTP POST for writes). WebSockets need an L4 load balancer (bare TCP), should terminate into a dedicated WebSocket service early to keep other services stateless, and require handling long-lived connections, deployments/reconnection, and least-connections balancing.
Consistent hashing vs Pub/Sub for hop 2 — how do you decide?
Ask how stateful your services are. Heavy per-connection state (e.g. a Google Doc’s editing state) → consistent hashing, which pins each user to one server so state lives in one place and scaling moves only a fraction of connections. Small, stateless message passing → Pub/Sub (the default): endpoint servers are dumb proxies, state lives in the Pub/Sub service, and any user can connect to any server.
What is the thundering-herd risk in consistent hashing, and how is it mitigated?
If you drop all users off a server at once (or kill a server mid-transition), you trigger mass reconnection — and new connections are resource-heavy, causing more churn. Mitigate by formally signaling the scaling event, keeping both old and new assignments live during the transition (dual-sending messages), and draining connections slowly with back-pressure so new servers absorb them gradually.
Redis vs Kafka for Pub/Sub — what decides it?
Durability. Redis Pub/Sub is in-memory, no durability — ideal fire-and-forget (e.g. a stock ticker where only the latest value matters). Kafka provides replay logs, so when every update must eventually reach users you can store messages in the topic and replay what a reconnecting client missed.
Why can a huge chat room push you from consistent hashing to Pub/Sub?
With consistent hashing, everyone in a room maps to the same server, so a room with millions of users overloads a single server. Pub/Sub spreads those connections across interchangeable endpoint servers and broadcasts via topics, avoiding the single-server hotspot.
14Quick revision
The frame
Two hops: Hop 1 = server→client (protocol). Hop 2 = source→server (routing). Solve separately. Start with polling; escalate only for genuine sub-second needs. WebSockets are a last resort.
Hop 1 protocols at a glance
| Protocol | Direction | Best for | Main cost |
|---|---|---|---|
| Simple polling | client pulls | Not latency-sensitive; short windows | Latency; wasted requests |
| Long polling | client pulls (held) | Infrequent near-real-time (e.g. payment status) | Bad for high frequency; hidden state |
| SSE | server → client | One-way push, high frequency (AI token streams) | One-way; proxy buffering breaks it |
| WebSockets | bidirectional | Frequent two-way (chat, collab edit) | Stateful; L4 LB; deploy/reconnect complexity |
| WebRTC | peer ↔ peer | Audio/video, gaming | Signaling + STUN/TURN; complex |
Hop 2 patterns
- Pull polling — server queries a DB; sidesteps routing; high latency; watch read TPS (1M clients / 10s = 100k TPS).
- Consistent hashing — pin user→server via a ring (ZooKeeper + hash fn). Best when connection state is heavy. Deep dives: orchestration, dual-send during transitions, drain to avoid thundering herd, state lost on failure.
- Pub/Sub — default. Endpoint servers are dumb proxies; state in the service. Redis (no durability, fire-and-forget) vs Kafka (replay). SPOF/bottleneck → Redis cluster + least-connections LB.
Decision cues
- Hop 1: latency-sensitive? → no: polling · one-way: SSE · bidirectional: WebSockets · audio/video: WebRTC.
- Hop 2: heavy per-connection state? → consistent hashing · else Pub/Sub.
- Infra: WebSockets → L4 LB + terminate early; SSE/long-poll → don’t let proxies buffer or hang up; endpoint servers → least-connections.
Deep-dive triggers
- Reconnection: heartbeats for zombie connections; sequence numbers / Redis streams to replay missed messages.
- Celebrity fan-out: cache once + hierarchical/regional distribution.
- Ordering: single server/partition for a total order (vector clocks only for deep infra).