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.
The problem
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 / intervalreq/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
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.
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 */ }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 | Long polling | SSE | WebSocket | |
|---|---|---|---|---|
| Directionality | Client asks | Client asks, server delays answer | Server → client stream; client uses normal requests | Both directions on one connection |
| Latency of a push | interval / 2 | ≈ 1 RTT after event | ≈ 0 after event (stream is open) | ≈ 0 after event |
| Idle cost | clients / interval req/s | One open request per client | One open response per client | One open connection per client |
| Transport | Plain HTTP | Plain HTTP | Plain HTTP (h1 chunked or h2 stream) | HTTP upgrade → WebSocket frames (h1); RFC 8441 over h2 partially supported |
| Reconnection | Not needed | Client loop | Built into EventSource with Last-Event-ID | Application must implement backoff and resume |
| Proxy / LB friendliness | Perfect; cacheable | Needs timeout > hold time | Needs no buffering, long idle timeout | Needs upgrade support, long idle timeout, connect-time pinning |
| HTTP/2 multiplexing | Yes | Yes | Yes — many streams on one connection | No (h1 connection per socket unless RFC 8441) |
| Scale | Origin load grows with clients; CDN can absorb | Connections held; fine on event-driven servers | Connections held; fan-out via pub/sub | Connections held; fan-out via pub/sub; sticky |
| Browser support | Universal | Universal | All modern browsers (EventSource) | All modern browsers |
| Payload | Any | Any | UTF-8 text | Text or binary |
Deciding, and what is coming
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 / intervalrequests 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-streamHTTP response with automatic reconnection andLast-Event-IDresume; 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 | SSE | WebSocket | |
|---|---|---|---|
| Avg latency to deliver | 1.3 s | 0.1 s | 0.1 s |
| HTTP requests / 30 s (100 clients) | 11 × = 1,100 | 2 × = 200 | 1 × = 100 |
| Open connections held | 0 (short-lived) | 100 | 100 |
| Direction | client → server, answers ride back | server → client only | both, any time |
| Infrastructure | plain HTTP, cacheable | plain HTTP, needs buffering off | Upgrade support on every hop |
| Reconnection | n/a | automatic, Last-Event-ID resumes | do it yourself (backoff, replay) |
| Scale | cost ∝ clients / interval | one idle connection per client | one idle connection per client |
| Proxy friendliness | excellent | good (HTTP/2 friendly) | fragile without config |
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=1or 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.