Support

Lesson 9 of 11 ·11 min read ·Reviewed 7 August 2026

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
WhenApp closed or backgroundedApp open, screen active
LatencySeconds, sometimes minutesSub-second
ReliabilityBest-effort, OS-mediatedOrdered 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.

Rung 1

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.

Rung 2

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.

Rung 3

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.

Rung 4

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:

WebSocket upgrade handshake (HTTP/1.1)
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.

Chat messages (sample)
-> { "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:

  1. 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.
  2. 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.
  3. 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.)
  4. 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.
  5. 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:

  1. Client calls a normal REST endpoint with its bearer token: POST /ws/ticket.
  2. Backend returns a short-lived, single-use ticket (seconds of validity).
  3. 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.

OptionExamplesTrade
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

NeedUse
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 closedPush, 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

SymptomCause
Connections silently die after exactly N secondsA proxy/load balancer idle timeout shorter than your heartbeat interval; heartbeat more often, or configure the timeout
Works locally, fails in productionLoad balancer or CDN not configured to pass the Upgrade handshake / long-lived connections
Messages sent during a brief disconnect vanishNo resume cursor; the socket was treated as the truth (§5 violated)
Deploys cause outage-shaped traffic spikesReconnection storm without jitter or connection-rate limiting
The same chat message appears twiceReconnect overlap without de-duplication by event ID
Battery complaints from mobile usersThe 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 agoPresence built on connects without TTLs; servers crash without sending disconnects
A user receives events from a room they never joinedSubscriptions authorized by token validity alone; every join and send needs a resource-level permission check (§6)
Events vanish when a real-time server restartsThe 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 addedWSGI 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 Protocols response, 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.