Real-timepollinglong pollingsseserver-sent eventseventsource

Polling vs Long Polling vs SSE vs WebSockets

Four ways to get data from a server that has news to a client that only asks: repeated requests, requests the server holds open, a one-way HTTP stream with built-in reconnection, and a bidirectional socket — each trades infrastructure simplicity against latency and directionality.

ConceptualBrowser
▶ InteractiveInterview question
Progress

The problem

Ten thousand browsers want to know when an order’s status changes, and a status changes about once a minute per user. Every option either has the clients ask repeatedly, has the server hold connections open, or replaces HTTP altogether — and each choice decides what your proxies must support, how reconnection works, and what it costs at scale.

Polling and long polling

Polling is the option that needs nothing: the client sends GET /orders/42 every *n* seconds. The cost is arithmetic — clients / interval requests per second whether or not anything changed: 10,000 clients at 5 s is 2,000 req/s of mostly 304 Not Modified, and the average notification delay is *n*/2. Polling is right when updates are rare, when the client population is small, or when the response is cacheable at a CDN so the origin sees one request per interval rather than one per client. Over HTTP/2 the per-request overhead is small; over HTTP/1.1 each poll is a full header set, and cold connections make it worse (Keep-Alive and Connection Reuse).

Long polling flips the wait onto the server: the client sends a request and the server does not answer until there is news or a timeout (typically 20–30 s) expires, at which point the client immediately asks again. Latency drops to one RTT after the event and idle cost to one request per timeout. The costs: each waiting client holds a connection and, on a thread-per-request server, a thread; a proxy or balancer with a shorter timeout than the server’s hold time returns 504s; and HTTP/1.1 browsers spend one of their ~6 per-origin connections on the pending request. It works through every proxy and firewall on Earth, which is why it remains the fallback of choice.

  • Polling cost: clients / interval req/s; latency ≈ interval / 2; zero infrastructure requirements.
  • Long polling: latency ≈ 1 RTT after the event; one open request per client at all times; proxy timeouts must exceed the hold time.
  • Both are plain HTTP: cacheable, retry-able, debuggable with curl.

Server-Sent Events: a one-way stream over HTTP

Browser

SSE is an ordinary HTTP response that never ends. The client sends GET /events, the server answers 200 with Content-Type: text/event-stream, and keeps writing data: lines separated by blank lines — chunked over HTTP/1.1 (HTTP/1.1: Persistent Connections and Their Limits), a long-lived stream over HTTP/2, no upgrade, no new protocol. The browser’s EventSource API parses events, dispatches named event: types, and — the feature that makes SSE operationally pleasant — reconnects automatically after a drop, sending the last id: it saw in a Last-Event-ID header so the server can resume from where the client left off. The retry: field sets the reconnect delay.

It is one direction only: server to client. The client sends its own data with normal requests, which is fine for the majority of "push" needs (notifications, live scores, progress, log tailing, the token stream of an LLM response). SSE is text — UTF-8 only; binary must be encoded. Over HTTP/1.1 each stream occupies one of the browser’s ~6 connections per origin, which limits tabs; over HTTP/2 streams are multiplexed and the limit is the server’s stream setting, so SSE and HTTP/2 are a natural pair (HTTP/2: Streams on One Connection).

Infrastructure needs are modest but specific: proxies must not buffer the response (nginx proxy_buffering off or the X-Accel-Buffering: no header; Cache-Control: no-cache), compression must be streaming or off, and idle timeouts at the balancer must be longer than the gap between events, or the server must send a comment line (: keepalive) periodically. Every one of these is a configuration, not a protocol change.

The client side is one line; reconnection and Last-Event-ID are the browser’s job
1const es = new EventSource('/events', { withCredentials: true })
2es.addEventListener('status', (e) => render(JSON.parse((e as MessageEvent).data)))
3es.onerror = () => { /* browser reconnects on its own with Last-Event-ID; log, do not reconnect manually */ }
An SSE response: three events, one with an id the client will send back after a reconnect
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
X-Accel-Buffering: no

retry: 3000

id: 1041
event: status
data: {"orderId":42,"status":"packed"}

: keepalive

id: 1042
event: status
data: {"orderId":42,"status":"shipped"}

The comparison

WebSockets cover the fourth option: a bidirectional framed connection with the lowest per-message overhead and the highest infrastructure demands. The matrix puts the four side by side on the properties that actually decide the choice.

Polling vs long polling vs SSE vs WebSocket
PollingLong pollingSSEWebSocket
DirectionalityClient asksClient asks, server delays answerServer → client stream; client uses normal requestsBoth directions on one connection
Latency of a pushinterval / 2≈ 1 RTT after event≈ 0 after event (stream is open)≈ 0 after event
Idle costclients / interval req/sOne open request per clientOne open response per clientOne open connection per client
TransportPlain HTTPPlain HTTPPlain HTTP (h1 chunked or h2 stream)HTTP upgrade → WebSocket frames (h1); RFC 8441 over h2 partially supported
ReconnectionNot neededClient loopBuilt into EventSource with Last-Event-IDApplication must implement backoff and resume
Proxy / LB friendlinessPerfect; cacheableNeeds timeout > hold timeNeeds no buffering, long idle timeoutNeeds upgrade support, long idle timeout, connect-time pinning
HTTP/2 multiplexingYesYesYes — many streams on one connectionNo (h1 connection per socket unless RFC 8441)
ScaleOrigin load grows with clients; CDN can absorbConnections held; fine on event-driven serversConnections held; fan-out via pub/subConnections held; fan-out via pub/sub; sticky
Browser supportUniversalUniversalAll modern browsers (EventSource)All modern browsers
PayloadAnyAnyUTF-8 textText or binary

Deciding, and what is coming

Browser

The decision is mostly about directionality and frequency. If updates are rare and clients are few, poll — it is the cheapest thing to operate. If the server pushes and the client speaks only occasionally through normal requests, SSE is enough for most "real-time" products: notifications, feeds, dashboards, progress bars, streaming responses; it needs no new infrastructure, reconnects on its own, and multiplexes over HTTP/2. Choose WebSockets when the client also sends frequently and latency matters in both directions — chat with typing indicators, multiplayer state, collaborative cursors, trading — or when binary framing matters. Keep long polling as the fallback that works when a corporate proxy breaks the others.

The emerging option is WebTransport: a browser API over HTTP/3 and QUIC that offers multiple independent streams (no head-of-line blocking between them) and unreliable datagrams for data where a late update is worse than a lost one — game state, media. It inherits QUIC’s properties and costs from HTTP/3 and QUIC, including the requirement that UDP/443 be reachable. Browser support is still uneven (Chromium-based browsers and Firefox ship it; Safari support lags as of 2026) and server support is limited to QUIC-capable stacks, so today it is a specialised tool, not a default; design with a WebSocket fallback.

  • Rare updates, few clients → polling. Server push, occasional client requests → SSE. Frequent bidirectional traffic → WebSocket. Hostile proxies → long polling fallback.
  • SSE covers most "push" needs and is the least infrastructure change from plain HTTP.
  • WebTransport: streams + datagrams over QUIC; promising for games and media; not yet a safe default.

Key points

  • Polling costs clients / interval requests per second and a latency of half the interval; it needs nothing and is CDN-cacheable.
  • Long polling holds the request open until there is news; one RTT latency, one open request per client, works through any proxy if timeouts allow.
  • SSE is a never-ending text/event-stream HTTP response with automatic reconnection and Last-Event-ID resume; server→client only, text only, multiplexes over HTTP/2.
  • WebSockets are bidirectional and framed but need upgrade-aware proxies, heartbeats, a reconnect loop and pub/sub for fan-out.
  • SSE is sufficient for most push needs; choose WebSockets when the client sends frequently too or needs binary.
  • Proxies must not buffer SSE, and idle timeouts at every hop must exceed the gap between events for any held-open design.
  • WebTransport (HTTP/3 streams and datagrams) is the emerging option; browser and server support are still uneven.

Why does this exist?

Mechanisms are answers to constraints. Open each question before reading the answer.

Why is long polling still used when SSE exists?

It is indistinguishable from a slow HTTP request to every proxy, firewall and corporate inspection device. When SSE streams are buffered or WebSocket upgrades are stripped, long polling still works.

Why does SSE get automatic reconnection and WebSocket does not?

SSE is an HTTP response, so the browser knows what "the same request again" means and can add Last-Event-ID; a WebSocket carries application-defined messages, so only the application knows what state to resume.

Why did SSE become more attractive with HTTP/2?

Under HTTP/1.1 each SSE stream consumed one of the browser’s ~6 connections per origin, so a few tabs starved ordinary requests. Under HTTP/2 each stream is one of hundreds on a single connection.

Why would anyone want unreliable datagrams (WebTransport)?

For state that is superseded by the next update — a player position, a video frame — retransmitting a stale one delays the fresh one. Dropping it is better than delivering it late, and TCP and WebSocket cannot drop anything.

Polling vs SSE vs WebSockets

Polling vs SSE vs WebSockets
The server has news at t = 7, 12 and 25 s. Three ways to get it to the client over a 30-second window.
Clients
Client sends too (t = 9, 20)
Simulated
news 7snews 12snews 25s
Polling
GET /updates every 3 s
SSE
one GET, text/event-stream
WebSocket
one upgrade, frames both ways
HTTP requestnews delivered to clientclient → server messageconnection droppedconnection opened
PollingSSEWebSocket
Avg latency to deliver1.3 s0.1 s0.1 s
HTTP requests / 30 s (100 clients)11 × = 1,1002 × = 2001 × = 100
Open connections held0 (short-lived)100100
Directionclient → server, answers ride backserver → client onlyboth, any time
Infrastructureplain HTTP, cacheableplain HTTP, needs buffering offUpgrade support on every hop
Reconnectionn/aautomatic, Last-Event-ID resumesdo it yourself (backoff, replay)
Scalecost ∝ clients / intervalone idle connection per clientone idle connection per client
Proxy friendlinessexcellentgood (HTTP/2 friendly)fragile without config
If data only flows server → client, SSE gives you push with plain HTTP, automatic reconnection and Last-Event-ID resume after the drop at t = 15 s. Polling stays fine for slow-changing data and is the easiest to cache and debug. Toggle "client sends too" to see when WebSockets become the natural choice.
1/31 · t = 0 s

How it fails

What the failure looks like from inside real software.

  • SSE events arrive in bursts every few seconds instead of immediately: a proxy is buffering the response; disable buffering / send X-Accel-Buffering: no.
  • Polling at 1 s from 50,000 clients turns into 50,000 req/s at the origin; the fix is a CDN with max-age=1 or switching to SSE.
  • Long polling behind a balancer with a 30 s timeout and a 45 s server hold: every idle client gets a 504 every 30 s.
  • SSE over HTTP/1.1 with six tabs open: the seventh request to the origin queues indefinitely — connection-per-origin limit reached.
  • WebSocket chosen for a notifications feature; the team then rebuilds reconnection, resume and fan-out that SSE would have provided or avoided.
  • WebTransport-only client fails silently on a network that blocks UDP; no fallback was implemented.