WebSocket patterns
A websocket upgrades an HTTP connection into a persistent bidirectional channel. Either side can send at any time, which is what makes genuine realtime possible — as against polling, where the client asks repeatedly and latency is bounded by the interval.
The protocol is straightforward. The difficulty is entirely in what surrounds it, and it comes from one property: a websocket is stateful in a stack built around statelessness.
Authentication
The browser WebSocket API does not permit custom headers on the handshake, so the standard Bearer token mechanism is unavailable. The options are:
- Token in a query parameter. Simple, and it puts the credential in server access logs and proxy logs, where it persists.
- Token in the first message after connecting. Keeps it out of logs, at the cost of a connected-but-unauthenticated state that must be handled — including a timeout for connections that never authenticate.
- Cookie-based, which works since cookies are sent on the handshake, and requires the CSRF handling cookies always require.
I have used the first-message approach, on the grounds that credentials in logs are a durable problem while an extra connection state is contained. None of these is clean, and expiry mid-connection has no good answer at all — a long-lived socket either outlives its credential or gets dropped when the token expires.
State and scaling
Once there are two application processes, connections for the same logical session may land on different ones. In-process memory is not shared, so broadcasting a message to a room only reaches the clients that process happens to hold.
The fix is external coordination — Redis pub/sub, so any process can publish and every process delivers to its own connections. This is not an optimisation; it is required the moment there is more than one worker, and it is invisible in development where there is one.
Connection state also needs somewhere to live. Who is in which room, who is connected: durable enough to be shared, ephemeral enough not to belong in PostgreSQL. That is the split Study Duel settled on.
Operational reality
Connections drop — mobile networks, laptop sleep, proxy timeouts — so clients need reconnection with backoff, and the server must handle a client reappearing with state to reconcile. Heartbeats detect dead connections that TCP has not noticed yet.
Reverse proxies need explicit upgrade handling and much longer timeouts than REST endpoints. Traefik handles this, and a default timeout will silently kill idle sockets.
Not every realtime requirement needs websockets. Server-sent events are simpler and sufficient when only the server sends, and polling is entirely adequate at low frequency. Websockets earn their complexity when both directions are genuinely needed.
See also: HTTP, Redis, FastAPI, Traefik, and Study Duel.