Filter concepts by levelShowing all levels.

System Design · Real-Time Systems

Real-Time Systems

Four transports, a six-factor decision framework, and what a live connection actually costs to run

  • Four transportspolling, long polling, SSE, WebSockets
  • Six factorsdirectionality, count, frequency, ordering, infra, clients
  • HeartbeatTCP alone won't catch a silently dead peer
  • Backpressureone slow client's buffer, bounded

1.Polling, long polling, SSE and WebSockets

These are the four standard ways a client gets near-real-time data from a server. Polling repeats a plain request every few seconds regardless of whether anything changed. Long polling holds each request open until the server actually has something to say, then the client immediately reopens it. Server-Sent Events (SSE) is a single long-lived HTTP connection the server keeps pushing text events down, one direction only, with the browser reconnecting automatically if it drops. WebSockets upgrade one HTTP handshake into a persistent, full-duplex socket where either side can send a message at any time. Each option trades implementation simplicity and infrastructure compatibility for how immediate and how bidirectional the communication can be.

Key features

  • Polling: a fresh HTTP request on a fixed interval — simplest to build and debug, but wastes requests when nothing changed and adds up to interval-length latency
  • Long polling: the server holds the request open (no response) until it has data or a timeout hits, then the client reopens immediately — lower latency than polling, but still one request-response cycle per message, and each open request ties up a server thread/connection
  • SSE: one persistent HTTP connection, server-to-client only, text-based (the EventSource API), with automatic reconnection built into the browser client — no custom reconnect logic needed
  • WebSockets: one HTTP handshake upgrades the TCP connection to a full-duplex socket (ws:// or wss://) — either side can push a message at any time, framed as messages rather than HTTP requests
  • Going down the list — polling to long polling to SSE to WebSockets — each step trades more server-side connection state and infrastructure complexity for lower latency and (for WebSockets) two-way communication

The four transports compared

The four transports compared
TransportDirectionLatencyConnections held openInfra fit
PollingClient-initiated onlyUp to one poll intervalNone (short-lived requests)Works everywhere — plain HTTP, any proxy/load balancer
Long pollingClient-initiated, server delays replyNear-immediate once server has dataOne per client, held until data or timeoutWorks almost everywhere, but ties up server threads/workers per waiting client
SSEServer → client onlyImmediate (server pushes as events occur)One long-lived HTTP connection per clientPlain HTTP/HTTPS — works through most proxies; needs HTTP/2 to escape the 6-connections-per-domain browser cap
WebSocketsFull-duplex, either sideImmediate, both directionsOne persistent TCP connection per clientNeeds upgrade support end-to-end — some older proxies/load balancers and corporate firewalls block or mishandle it
Four transports, increasing immediacy and cost

Polling

fixed interval, always wasteful

Long polling

held open until data arrives

SSE

server push, one direction

WebSockets

full-duplex, most infra cost

  1. Polling — fixed interval, always wasteful
  2. Long polling — held open until data arrives
  3. SSE — server push, one direction
  4. WebSockets — full-duplex, most infra cost

Command

text
// SSE — server-to-client only, browser auto-reconnects
const es = new EventSource('/stream');
es.onmessage = (e) => console.log(e.data);

// WebSocket — full-duplex, single connection
const ws = new WebSocket('wss://example.com/ws');
ws.onmessage = (e) => console.log(e.data);
ws.send('client can push too');

Polling: simplest, wastes requests, laggy. Long polling: lower latency, one request per message, ties up a server slot while waiting. SSE: server push over plain HTTP, one direction, auto-reconnect built in. WebSockets: full-duplex, needs upgrade support everywhere in the path, most infrastructure cost.

2.Six factors for choosing a real-time transport

Picking between polling, long polling, SSE and WebSockets is not a matter of taste — six concrete factors narrow it down. Directionality asks whether the client ever needs to push data, not just receive it. Connection count asks how many simultaneous clients the server has to hold state for. Message frequency asks whether updates are rare (minutes apart) or constant (many per second). Ordering asks whether messages must arrive in the order they were sent. Infrastructure asks whether every hop between client and server — load balancers, proxies, corporate firewalls — actually supports the transport. Client support asks whether the client (browser, mobile app, IoT device, another backend service) has a usable library for it. Running a real scenario through all six, in order, usually eliminates all but one option.

Key features

  • Directionality: server-to-client only rules WebSockets out as unnecessary — SSE covers it with less infrastructure cost; any client-to-server push requirement rules SSE out entirely
  • Connection count: holding open connections for long polling, SSE or WebSockets consumes server memory and (for thread-per-connection servers) worker threads per client — this is the factor that most often forces an event-loop-based server (Node.js, or async frameworks) instead of a traditional thread-per-request one at scale
  • Message frequency: sub-second updates make WebSockets' lower per-message overhead worth its setup cost; updates every few minutes make polling's simplicity worth its wasted requests
  • Ordering: a single WebSocket or SSE connection delivers messages in the order the server sent them (TCP guarantees this within one connection); polling and long polling have no such guarantee if two updates race across separate requests
  • Infrastructure: WebSockets require the HTTP Upgrade handshake to survive every proxy, load balancer and firewall on the path — some corporate networks and older infrastructure block or strip it; polling and SSE are plain HTTP requests, which nothing in the path treats specially
  • Client support: browsers support all four natively; some environments (older embedded devices, certain IoT stacks, some server-to-server clients) have solid HTTP support but no WebSocket library — polling is the fallback of last resort in exactly that gap

Worked scenario: a live sports-score widget on a public website

Worked scenario: a live sports-score widget on a public website
FactorWhat the scenario needsWhat it rules out
DirectionalityServer pushes score updates; client never sends anythingWebSockets (unnecessary two-way channel)
Connection countPotentially hundreds of thousands of simultaneous viewersAnything that needs a server worker thread blocked per client
Message frequencyA handful of updates per minute (goals, score changes)Nothing yet — low frequency fits several options
OrderingScore updates must apply in the order they happenedPolling/long polling with overlapping requests, if a naive client fired more than one at once
InfrastructurePublic website behind a standard CDN/load balancer, plain HTTPSNothing — SSE and polling both run over plain HTTP
Client supportModern browsers only, no legacy or embedded clientsNothing — EventSource is broadly supported
ConclusionSSE fits every factor: one-directional, cheap per connection relative to WebSockets, plain HTTP, ordered per connection, broad support—
Same six-factor funnel, opposite conclusions

Sports-score widget → SSE

  • Server pushes only; client never sends
  • Hundreds of thousands of viewers
  • A few updates per minute

Collaborative editor → WebSockets

  • Every keystroke must reach every other editor
  • Dozens of concurrent editors per document
  • Many edits per second while typing
  • Sports-score widget → SSE
    • Server pushes only; client never sends
    • Hundreds of thousands of viewers
    • A few updates per minute
  • Collaborative editor → WebSockets
    • Every keystroke must reach every other editor
    • Dozens of concurrent editors per document
    • Many edits per second while typing

Signature

directionality → connection count → frequency → ordering → infrastructure → client support

Six factors, roughly in filter strength: directionality (two-way instantly rules out SSE/polling), connection count (state cost per client), message frequency (per-message overhead vs setup cost), ordering (needed within one connection, not across separate requests), infrastructure (does Upgrade survive every hop), client support (does the target client have a usable library).

3.Connection lifecycle and backpressure

A long-lived connection — long polling, SSE or a WebSocket — goes through a lifecycle a plain request/response never has to think about: connect (handshake), an open period that can last minutes or hours, detecting that the other side silently died (a dropped WiFi connection does not politely close the socket), and reconnecting without losing or duplicating messages. Backpressure, in this specific setting, is what happens when a server produces messages faster than one particular client's connection can absorb them — a slow mobile connection, a paused browser tab, a client stuck in a slow render loop. Without a plan for it, the server's per-connection send buffer grows without bound until that one slow client exhausts server memory or gets messages arbitrarily delayed.

Key features

  • Lifecycle stages: handshake/connect, open (message flow in one or both directions), heartbeat/ping-pong to detect a silently-dead peer, and close (client- or server-initiated) followed by reconnect logic on the client
  • TCP alone does not reliably detect a peer that vanished without closing cleanly (WiFi drop, laptop lid closed, cell tower handoff) — WebSocket ping/pong frames and SSE's own reconnect timer exist specifically to catch this
  • Per-connection backpressure is distinct from queue backpressure (see system-design.messaging-and-queues.decoupling-with-queues for the general concept): here the "queue" is a single client's socket send buffer, and the slow consumer is one specific client, not the whole downstream system
  • Three concrete responses to a slow client: bound the per-connection buffer and drop the oldest/least-important messages once full, close the connection and let the client reconnect and resync, or apply a coalescing strategy that collapses several updates into one (e.g. only the latest price, not every intermediate tick)
  • An unbounded per-connection send buffer is the classic failure mode: one slow client keeps accumulating messages in server memory indefinitely, and enough slow clients simultaneously can exhaust server memory well before any single client's buffer alone looks alarming

Three ways to handle a slow client's connection filling up

Three ways to handle a slow client's connection filling up
StrategyWhat happensGood fit
Bounded buffer + drop oldestBuffer caps at N messages; oldest is discarded to make room for newLive dashboards, ticker feeds — staleness matters more than completeness
Disconnect and resyncServer closes the connection once the buffer is full; client reconnects and fetches current state freshState that can be fully resynced cheaply — e.g. "give me the current board," not a message log
Coalesce updatesMultiple pending updates to the same value collapse into one before sendingHigh-frequency updates to a small set of keys, e.g. a stock price or a live counter
A long-lived connection's lifecycle
succeedsheartbeattimes outclean close

Handshake

start

Open

Detected dead

end

Closed

end

  • Handshake (start)
    • → Open when succeeds
  • Open
    • → Detected dead when heartbeat times out
    • → Closed when clean close
  • Detected dead (end)
  • Closed (end)

Command

text
// WebSocket ping/pong heartbeat — server side (concept)
setInterval(() => {
  for (const socket of openSockets) {
    if (!socket.gotPongSinceLastPing) {
      socket.terminate();   // assume dead, free the slot
      continue;
    }
    socket.gotPongSinceLastPing = false;
    socket.ping();
  }
}, HEARTBEAT_INTERVAL_MS);

Lifecycle: handshake, open, heartbeat (TCP alone won't tell you a peer silently vanished), close, reconnect. Backpressure here means one slow client's own buffer, not queue depth — bound it and pick one policy: drop-oldest, disconnect-and-resync, or coalesce.

Remember

  • Polling: simple, wasteful, up to interval-length latency. Long polling: lower latency, ties up a connection per open request.
  • SSE: one-directional server push, automatic browser reconnect. WebSockets: full-duplex, persistent, the most infrastructure-sensitive.
  • Run a real scenario through all six choosing factors in order — it usually eliminates all but one transport.
  • A TCP socket can look open long after the real network path died — heartbeat, don't trust silence.

Pro tip

  • Bound every per-connection send buffer and pick one policy up front: drop-oldest, disconnect-and-resync, or coalesce.
  • Check infrastructure support (proxies, corporate firewalls, load balancer idle timeouts) before committing to WebSockets — it is the factor most often skipped until production.

Next up

  • WebSockets at scale
  • Batch and stream processing
  • Backpressure

Sources

  • MDN WebSockets API — developer.mozilla.org
  • Checked 2026-08-22

Spotted a mistake on this page? Tell us.