A realtime system is one where the server pushes data to the client without the client having to ask. Chat, dashboards, multiplayer state, live presence, sports scores, notifications — these all needle-by-needle to push from server to client, and none of them work over plain HTTP request/response.
The space is dominated by three transports — long-polling, Server-Sent Events (SSE), and WebSockets — and each has a shape where it wins. This topic is how to choose between them, and the connecting glue (presence, fan-out, backpressure) that makes any of them scale.
The Realtime Transport Spectrum
| Transport | Directionality | Cost over a minute of idle | Best fit |
|---|---|---|---|
| HTTP polling | Client → server | N requests/min — wasteful | Never; the historical fallback |
| Long-polling | Server responds when data arrives (or timeout) | 1 connection/sec = 60 idle requests/min | Worst case: when only a single event is expected |
| Server-Sent Events (SSE) | Server → client, unidirectional | 1 connection/min — true push | One-to-many push: stock tickers, news feeds, log streams |
| WebSocket | Bidirectional, message-based | 1 connection/min — true push | Interactive bidirectional: chat, multiplayer, collaborative editing |
| WebTransport (HTTP/3-based) | Bidirectional, low-latency | 1 connection/min — true push | The HTTP/3-native successor to WebSocket |
Three orthogonal axes matter for the choice:
- Who speaks when? Server-only is fine for SSE; both-need-to-talk is WebSocket.
- Through what middleboxes? corporate proxies and HTTP/2-only CDNs sometimes break long-lived WebSocket connections; SSE is more proxy-tolerant.
- What transport matters across the stack? A WebSocket is its own protocol; SSE rides on HTTP (with HTTP/2 multiplexing); if the surrounding infrastructure speaks only HTTP, SSE is the better fit.
Long-Polling
The client sends a request that the server holds open until an event is available (or a time limit). When data arrives, the server responds; the client immediately issues a new request.
- Pro: works through every proxy, every firewall, every CDN; pure HTTP.
- Con: every event costs one HTTP request/response overhead; under load, the overhead dominates.
- Modern use: kept only as a fallback when SSE or WebSocket is blocked.
Server-Sent Events (SSE)
SSE is a unidirectional server → client stream on top of HTTP. The server sends Content-Type: text/event-stream; the client opens an EventSource connection (the browser reconnects automatically).
GET /events HTTP/1.1
Accept: text/event-stream
HTTP/1.1 200 OK
Content-Type: text/event-stream
data: {"msg":"hello"}
event: status
data: {"value":1}
- Pro: native browser reconnect; HTTP/2 multiplexes many SSE streams on a single TCP connection; survives middleboxes; supports named events (
event: ping). - Con: server-to-client only; limited concurrent connections per browser on HTTP/1.1 (the limit cleared on HTTP/2).
- Modern use: log tails, status feeds, progressively-loaded search results, server pushes that don’t need client-side chatter.
WebSocket
WebSocket is a bidirectional, full-duplex, message-framed protocol that upgrades from an HTTP/1.1 handshake.
GET /ws HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: ...
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: ...
After the handshake, the connection switches from HTTP semantics to WebSocket frames — small length-prefixed messages, either text or binary, in either direction.
- Pro: bidirectional; smaller per-message overhead than HTTP; supports binary; constant connection enables quick back-and-forth.
- Con: requires its own server implementation; through some proxies/CDNs requires
wss://tunneling or fallback; no automatic reconnect (client code must handle). - Modern use: chat, multiplayer games, collaborative editors, tr traders’ real-time market data, voice/video data channels.
Connection Lifecycle and Heartbeat
Every persistent connection — SSE, WebSocket — must survive idle timeouts at every layer: the client, the user’s home router, the ISP, corporate proxies, the load balancer, the application server. Idle TCP connections over 5–10 minutes silently drop through Identity. Three discipline habits:
- Heartbeat: send a no-op frame every 10–30 seconds when no real traffic. Upgrades idle to alive.
- Reconnect on disconnect: both client and server should treat reconnects as ordinary. SSE has this in-browser; WebSocket needs custom code.
- Sequence numbers / versioning: on reconnect, the client can resume from the last sequence it received, and the server can replay missed messages. Without sequence numbers, a reconnect drops any message that fired during the dead window.
Presence and Fan-out Topologies
A realtime system that pushes to N users needs more than a transport — it needs a topology to route messages.
| Topology | Shape | When it works |
|---|---|---|
| Direct | Each client connects to one server; server pushes to that client | Single-server systems (≤ thousands of clients) |
| Hub / broadcast | All messages go to a central process; it fans out to clients | Medium-scale chat (Slack-like) |
| Pub/Sub | Clients subscribe to channels; servers publish to channels; a broker dispatches | Multi-tenant chat, multi-room — the canonical realtime pattern |
| Clustered | Each server holds its own subset of long-polling clients; a backplane (Redis Pub/Sub, NATS, Kafka) routes cross-server messages | Horizontal scale past a single server |
The publish/subscribe pattern is the workhorse: clients subscribe("room:42"), anyone publishes publish("room:42", payload), all subscribers on all servers see the payload.
Backpressure in Push Systems
A realtime system can push messages faster than the client can absorb them. Without backpressure, internal queues grow unbounded and the server eventually OOMs. Three patterns:
- Drop oldest — keep a fixed-size ring buffer; old messages replaced by new. Suitable for telemetry (the latest sample is what matters).
- Drop all — if the client has fallen behind by N messages, consider this connection dead and disconnect.
- Coalesce — collapse multiple updates into one. Suitable for presence (“user X is online” — only the latest matters).
The wrong choice manifests as latency blow-up: a slow client’s queue grows, the connection’s latency climbs, and a fresh reconnect is faster than catching up. Most production servers explicitly disconnect slow consumers — the live latency of the user is more valuable than the throughput.
Practice Trajectory
- Take an existing realtime flow in a system you know (chat, dashboard, multiplayer). Identify which transport it uses and why. Argue for and against the alternative transport.
- Implement an SSE endpoint that pushes a counter every second, with a
retrydirective on disconnect. Add a named event channel. Observe the browser reconnect. - Implement the same functionality over WebSocket; add a heartbeat and explicit reconnect on drop. Measure the difference in code complexity.
- Sketch a fan-out topology for a chat service with 100,000 concurrent users across 50 rooms. Pick the backplane (Redis Pub/Sub, NATS, Kafka); justify.
- Design the backpressure rule for a sports-score push service. Pick drop-oldest / drop-all / coalesce; argue why slow clients should disconnect.
When It’s the Right Tool
| Situation | Takeaway |
|---|---|
| Server pushes data to client, no client chatter | SSE — simplest, most proxy-tolerant; built-in browser reconnect |
| Bidirectional, low-latency, message-exchange | WebSocket |
| Server pushes events but only vents to one-shot consumers | Long-poll as a fallback through hostile proxies |
| Fan-out across many servers | Pub/Sub backplane; servers publish; clients subscribe |
| Push throughput exceeds client absorption | Defined backpressure policy and explicit disconnect for slow clients |