Real-Time and WebSockets
When request-response runs out of road: chat, live updates, and what they cost your backend.
1. The problem, and where push fits
HTTP is request-response: the client asks, the server answers, the exchange ends. That model is perfect until the server is the one with news: a chat message arrived, the score changed, a teammate moved a card. HTTP has no native way for the server to start the conversation.
Before reaching for anything, place real-time against push notifications. They solve different halves of the same problem and serious apps use both:
| Push (FCM/APNs) | Real-time channel | |
|---|---|---|
| When | App closed or backgrounded | App open, screen active |
| Latency | Seconds, sometimes minutes | Sub-second |
| Reliability | Best-effort, OS-mediated | Ordered while connected; durability is yours to build (§5) |
| Job | "Come back, something happened" | "Here it is, right now" |
A chat app runs a socket while you are in the app and falls back to FCM/APNs the moment you leave it.
2. The ladder of techniques
Like offline support, real-time is a ladder, and the bottom rungs are chronically underrated.
Short polling
Ask every N seconds. Wasteful (mostly empty responses), and latency equals the interval. But it is trivially simple, stateless, cache-friendly, and for a dashboard refreshing every 30 seconds it is the right answer. Do not let anyone shame you out of polling.
Long polling
The server holds the request open until it has news (or a timeout), answers, and the client immediately re-asks. Near-instant latency out of plain HTTP. Mostly a historical stepping stone ("Comet"), but still the fallback layer under Socket.IO when sockets are blocked.
Server-Sent Events (SSE)
One long-lived HTTP response down which the server streams data: events indefinitely. One direction only (server → client), but it is plain HTTP (proxies and load balancers cope), the browser's EventSource API has auto-reconnect with a resume position (Last-Event-ID) built in (the server still has to implement the replay), and the server side is otherwise simple. The workhorse for feeds, notifications, and dashboards. It is also how LLM APIs stream tokens: a streamed Claude or ChatGPT answer is SSE in action.
WebSockets
A persistent, full-duplex connection: both sides send at any time, sub-second, until someone hangs up. The complete answer to bidirectional real-time, and the point where your backend architecture changes (§4). HTTP/3's WebTransport is the newer, more capable sibling (multiple streams, unreliable datagrams), not yet a drop-in replacement.
3. What a websocket actually is
The connection starts life as an ordinary HTTP request that asks to stop being HTTP:
GET /ws HTTP/1.1
Host: api.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
HTTP/1.1 101 Switching Protocols <- the handshake succeeds
Upgrade: websocket
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
After the 101, the socket is no longer HTTP. The conceptual shift that follows:
There are no more requests. No verbs, no paths, no status codes, no headers per message, just messages (usually JSON) flowing both ways. Everything HTTP gave you for free, you now design yourself: a type field for routing ("message.new", "typing.start"), your own error signalling, your own acknowledgements. A websocket API is a small protocol-design exercise where a REST API was mostly convention.
-> { "type": "message.send", "temp_id": "c7a1", "room": "42", "text": "on my way" }
<- { "type": "message.ack", "temp_id": "c7a1", "id": 4018, "ts": "..." }
<- { "type": "message.new", "id": 4019, "room": "42", "from": "dara", "text": "grand" }
Plus heartbeats: ping/pong every ~30 seconds, because a silently dead connection (train tunnel, sleeping laptop) looks identical to a quiet one. No pong → close and reconnect. (Servers use the protocol's ping frames; browser JavaScript cannot send control frames, so browser clients heartbeat with application-level messages instead.)
4. The backend consequence: your servers become stateful
This is the section that explains why real-time is architecturally expensive.
Plain HTTP scales out easily because it is stateless: any server can answer any request, so a load balancer can spray traffic anywhere. A websocket breaks that: the live connection is pinned to one specific server's memory. When your code needs to tell user X something, it must find which server holds X's socket.
event: "msg for room 42"
(from any server or worker)
│
▼ publish
┌─────────────────────────┐
│ Redis pub/sub │
│ (backplane) │
└─────────────────────────┘
│ every server subscribes;
▼ room-42 holders deliver
Server A ── sockets: alice, bob
(knows nothing of carol)
Server B ── sockets: carol
(knows nothing of alice, bob)
The standard pieces:
- A pub/sub backplane (typically Redis): servers publish events to channels; every server subscribes and delivers to whichever of its own sockets care. No server needs a global connection map. The backplane is deliberately ephemeral (at-most-once delivery; a crashed subscriber's events are gone): durable truth lives in the database, which is why the replay layer in §5 exists.
- Connection registries / presence: knowing who is online means tracking connects and disconnects centrally, with TTLs, because servers crash without saying goodbye.
- Reconnection storms: a deploy or load-balancer blip drops 50,000 connections that all reconnect at once; backoff with jitter (client-side) and connection-rate limits (server-side) exist for this.
The Python angle: WSGI (the contract Flask speaks) is request-response; a WSGI worker cannot hold a websocket. Websockets in Python mean ASGI (FastAPI/Starlette, Django Channels) or bolting an async engine onto Flask (Flask-SocketIO with gevent). Many teams sidestep the issue entirely: the boring REST API stays as-is, and a separate small real-time service (or a managed provider) handles the sockets. The two halves meet at the database and the pub/sub bus, not in one process.
5. The reliability layer you must build on top
The socket will drop: phones hop between WiFi and cellular constantly. Production real-time is the raw transport plus:
- Reconnect with exponential backoff and jitter, automatically, for as long as it makes sense: a network blip should never end the loop, but a fatal close code or an authentication failure must stop it.
- Resume: every event carries an ID; on reconnect the client says "I have up to #4017" and the server replays the gap. This is the delta-sync cursor from Offline Data and Sync wearing a different hat: a socket is just a delta feed with very low latency.
- Persistence: messages are written to the database and pushed down sockets. The socket is a transport, not the truth. A client that missed the delivery must be able to fetch the same data over boring HTTP. (The single-source-of-truth rule, third appearance in the course.)
- Acknowledgements client→server (so the sender's UI can show sent/delivered) and de-duplication by ID, because reconnect overlap means the same event can arrive twice.
- Flow control: bound each connection's send queue, coalesce replaceable events (cursor positions, live counters) rather than queueing every one, and disconnect chronically slow consumers. Cap message size and rate on the way in: a socket bypasses the request-size limits your HTTP middleware enforces.
6. Auth on a websocket
Browsers cannot set an Authorization header on a websocket connection, so bearer-token habits break. The clean pattern is a ticket:
- Client calls a normal REST endpoint with its bearer token:
POST /ws/ticket. - Backend returns a short-lived, single-use ticket (seconds of validity).
- Client opens the socket with the ticket (
wss://api.example.com/ws?ticket=...); the server validates it, binds the connection to the user, and the ticket dies.
Long-lived tokens directly in the query string also "work" but leak into server and proxy logs; the ticket's short life is what makes the pattern safe. Native apps can set headers on the handshake, but the ticket pattern keeps web and native clients identical. After the handshake, the connection itself is the session; re-auth on reconnect, and have a revocation story for connections that outlive their credentials (close or re-check when tokens expire, permissions change, or the account is suspended).
Authentication is not authorization. The handshake binds the connection to an identity; every room subscription and state-changing message still gets its own permission check (may this user join room 42? send this message type here?), exactly as a REST endpoint checks more than the token's validity. Never trust a client-supplied room or user ID. Browser handshakes also carry an Origin header: check it against your allowed origins, because websockets sit outside CORS, and for cookie-authenticated handshakes an unchecked Origin invites cross-site hijacking.
7. Buy vs build
Real-time tilts further toward buy than anything else in this course, because the hard part (stateful fan-out at scale) is pure undifferentiated heavy lifting.
| Option | Examples | Trade |
|---|---|---|
| Managed real-time | Ably, Pusher, Supabase Realtime, Firebase | Channels, presence, history, and scale as a product; per-connection/message pricing and a vendor in your data path |
| Self-hosted engine | Socket.IO (reconnection + rooms + fallbacks bundled), Centrifugo, Phoenix Channels (the gold standard) | Full control, no per-message fees; you run the backplane and eat the ops |
| Raw websockets | ASGI + websockets/Starlette by hand |
Maximum learning, minimum leverage; you rebuild §5 yourself |
8. Choosing: the decision rule
| Need | Use |
|---|---|
| Updates may be seconds-to-minutes stale (dashboards, prices, planner data) | Polling, and stop there without embarrassment |
| Server → client streaming only (feeds, notifications, progress, LLM tokens) | SSE |
| Genuinely bidirectional, sub-second (chat, collaboration, multiplayer, live cursors) | WebSockets, via a managed service or engine, rarely raw |
| User must hear about it when the app is closed | Push, alongside whichever of the above |
Applied to the running example: the planner app needs none of this. Cache plus pull-to-refresh (no rung of this ladder at all) is correct, and that is a feature of the design, not a limitation.
9. Common gotchas
| Symptom | Cause |
|---|---|
| Connections silently die after exactly N seconds | A proxy/load balancer idle timeout shorter than your heartbeat interval; heartbeat more often, or configure the timeout |
| Works locally, fails in production | Load balancer or CDN not configured to pass the Upgrade handshake / long-lived connections |
| Messages sent during a brief disconnect vanish | No resume cursor; the socket was treated as the truth (§5 violated) |
| Deploys cause outage-shaped traffic spikes | Reconnection storm without jitter or connection-rate limiting |
| The same chat message appears twice | Reconnect overlap without de-duplication by event ID |
| Battery complaints from mobile users | The app holds a socket (radio awake) for data that changes once a minute; polling or push was the right rung |
| "Online" shows users who left hours ago | Presence built on connects without TTLs; servers crash without sending disconnects |
| A user receives events from a room they never joined | Subscriptions authorized by token validity alone; every join and send needs a resource-level permission check (§6) |
| Events vanish when a real-time server restarts | The pub/sub backplane treated as durable storage; it is at-most-once fan-out, and replay must come from the database (§5) |
| Flask endpoint "hangs" when a socket route was added | WSGI worker consumed by a long-lived connection; the ASGI/sidecar split in §4 |
The takeaway shape: real-time is a ladder (poll → long-poll → SSE → sockets): climb only as far as the product demands, because the top rung changes your backend from stateless to stateful, and that costs a backplane, a reliability layer, and a protocol of your own design. The socket is never the truth: it is a low-latency delta feed over the same single source of truth the sync lesson established, with the same cursors, the same idempotency, and boring HTTP always underneath as the fallback.
Check your understanding
- When is plain short polling the right answer, and what makes it defensible against "just use websockets"?
- What does the connection stop being after the
101 Switching Protocolsresponse, and what must you now design yourself? - Why does holding sockets require a pub/sub backplane, and why must the socket never be treated as the source of truth?
Sources and further reading
Reviewed 7 August 2026.