The shape mismatch
HTTP is client-asks, server-answers — perfect for "get my orders," structurally wrong for "tell me when something happens": chat messages, live prices, collaborative cursors, notifications. The server has news; the protocol only lets it speak when spoken to.
The escalation ladder every realtime feature climbs:
| Technique | How | Latency | Cost | Use when |
|---|---|---|---|---|
| Short polling | ask every N seconds | up to N | mostly wasted requests | N of minutes is fine (cron-ish) |
| Long polling | server holds the request until news (or timeout), client immediately re-asks | near-real-time | one parked connection per client; reconnect churn | WebSockets blocked (legacy infra) |
| SSE | one HTTP response that never ends; server streams events down it | real-time | one connection; server→client only | feeds, notifications, LLM token streams |
| WebSocket | full bidirectional socket over one connection | real-time | one connection + protocol machinery | chat, games, collab editing — client talks too |
SSE (Server-Sent Events) deserves its underdog respect: it's plain HTTP (proxies/load balancers just work), auto-reconnects natively, and covers every "server pushes, client just watches" case — which is most cases. The interview-grade instinct: reach for SSE first; pay for WebSocket only when the client needs to send continuously too. (Every LLM chat UI streaming tokens at you — Level 11 — is SSE.)
The WebSocket handshake
A WebSocket starts life as an HTTP request and upgrades:
GET /chat HTTP/1.1
Upgrade: websocket ← "let's switch protocols"
Connection: Upgrade
Sec-WebSocket-Key: x3JJh... ← anti-accident nonce
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Sec-WebSocket-Accept: HSmrc...
After the 101, the TCP connection stops being HTTP entirely — it's a
persistent, two-way frame pipe (text/binary, with ping/pong
heartbeats to detect dead peers). Starting as HTTP is the genius bit:
it traverses firewalls, ports and TLS (wss://) like any web traffic.
// client — the entire API surface
const ws = new WebSocket("wss://api.example.com/chat");
ws.onopen = () => ws.send(JSON.stringify({ type: "join", room: "dsa" }));
ws.onmessage = (e) => render(JSON.parse(e.data));
ws.onclose = () => reconnectWithBackoff(); // YOUR job — see below
# server (FastAPI — same idea in every framework)
@app.websocket("/chat")
async def chat(ws: WebSocket):
await ws.accept() # authenticate HERE, at upgrade time
rooms.join(ws, "dsa")
try:
while True:
msg = await ws.receive_json() # client → server
await rooms.broadcast("dsa", msg) # server → everyone
except WebSocketDisconnect:
rooms.leave(ws)
Note which runtime this wants: thousands of mostly-idle connections is the event-loop workload — Node, FastAPI/uvicorn, or WebFlux (StockStump's choice, for exactly its live-pricing fan-out).
What you now own (that HTTP gave free)
Adopting WebSockets means leaving HTTP's safety net. The checklist of what you're signing up to rebuild:
- Reconnection — mobile networks drop constantly; clients need auto-reconnect with jittered exponential backoff (everyone reconnecting at once after a server restart = thundering herd), plus resync of missed state ("what did I miss?" — a sequence number or a re-fetch on reconnect, WhatsApp's offline-inbox drain in miniature).
- Heartbeats — TCP can die silently; ping/pong every ~30 s detects zombie connections on both ends.
- Auth — there's no per-request header anymore: authenticate at upgrade (cookie/token, previous page), and decide a policy for token expiry mid-connection.
- Message framing — no verbs/paths/status codes; you design the
envelope (
{type, payload, seq}) and its versioning (contract rules apply to it too). - Backpressure — a slow phone on a fast feed: the server must drop, coalesce (send latest price, not every tick), or disconnect — unbounded send buffers are a memory leak with a timer.
Scaling: the pub/sub backplane
The architecture question this topic exists for. One process holding all sockets works until it doesn't; the moment you run two servers, user A (on server 1) messages user B (on server 2) — and server 1 has no socket to B.
The standard answer: servers are stateless-ish socket holders; all
cross-server delivery flows through a pub/sub backplane (Redis
pub/sub, Kafka). Server 1 publishes to channel room:dsa; every server
subscribed to that room delivers to its own local sockets. Add a
session registry (user → server,
the WhatsApp routing table) for direct messages.
Load balancers need sticky-ish behavior only insofar as a connection
lives on one box; new connections can land anywhere.
This is StockStump's pricing fan-out and WhatsApp's gateway tier — the same three-piece kit: socket holders + registry + backplane. Learn it once, recognize it everywhere.
Common mistakes
- WebSocket-by-default — for a notifications bell that updates server→client only, SSE (or even 30 s polling) ships in a tenth of the code. Match the tool to the arrow directions.
- No reconnect/resync design — works on office Wi-Fi, drops state on every elevator ride. The disconnect path is the feature.
- Broadcasting by looping over a DB query of connections — connections are in-memory objects on some server; that's why the backplane exists.
- Skipping heartbeats — zombie sockets accumulate until memory or file-descriptor exhaustion (the unclosed-file lesson at scale).
- Unbounded per-client queues — messages buffered for one slow consumer pile up until the server runs out of memory and crashes (an OOM — out-of-memory — kill); coalesce or cut.
- Auth only at connect, forever — an 8-hour socket outlives a 10-minute token; re-verify on a timer or on sensitive actions.
Interview perspective
Practice
- Build the ladder: a live counter page four ways — 5 s polling, long polling, SSE, WebSocket. Watch the network tab; feel each trade-off you read above.
- Chat room: FastAPI/Node WebSocket server with rooms, join/leave, broadcast; add ping/pong and kill zombie sockets. Then kill the server mid-chat and implement client backoff + resync-by-sequence-number.
- Scale it: run two instances behind nginx; break cross-instance chat, then fix it with Redis pub/sub. (You will never forget the backplane after seeing the silent failure.)
- Connect: trace StockStump's flow — feed → engine → Redis pub/sub → WebSocket gateways — and annotate which piece of this page each component is.
Next: Microservices — what happens when the backend itself splits apart.