LongPolling vs WebSockets vs ServerSent Events
All four of these run over the same TCP/HTTP plumbing; they differ on exactly one axis — how long the connection stays open and which direction bytes may flow once it is — and every trade-off below falls out of that single choice. A plain HTTP request is the degenerate case: the client opens a connection, the server computes a reply, sends it, and the connection is done. To get server-initiated updates you have to bend that cycle in one of four ways.
The four mechanisms, on one axis
- Polling — the client re-issues a normal HTTP request on a timer (say every 3s). The connection is closed between requests, so the server holds nothing. Simplest possible design; the cost is latency (up to one interval) and a flood of empty responses.
- Long-polling ("hanging GET") — the client sends a request, but the server does not answer immediately: it holds the request open until it has data or a timeout (typically 20–30s — always chosen below the idle timeout of every proxy/LB on the path, which is often 60s) fires, then responds. The client re-connects at once. Latency drops to ~one network round-trip, and empty responses mostly disappear — but you still pay a full set of HTTP headers per message.
- WebSockets — the client sends an HTTP
GETwithUpgrade: websocket; the server replies101 Switching Protocolsand the same TCP connection becomes a full-duplex, framed channel. Either side may send at any time, with a tiny 2–14 byte frame header. This is the only option that is genuinely bidirectional and supports binary. - Server-Sent Events (SSE) — the client does one
GETwithAccept: text/event-stream; the server respondsContent-Type: text/event-streamand keeps writingdata:events down the open response body forever. Data flows server → client only; to send upstream the client opens a separate normal request. In exchange for giving up the upstream direction, SSE hands you automatic reconnection and event resumption for free (below).
A traced example: a stock ticker
The server receives an AAPL price update at t = 2.0s (189.20) and again at t = 7.0s (190.05). The client wants each price as soon as possible. Poll interval = 3s; long-poll timeout = 30s. Watch when the client actually sees each update and how much is wasted.
| Approach | Network events in 0–9s | Sees 189.20 (@2.0s) | Sees 190.05 (@7.0s) | Waste |
|---|---|---|---|---|
| Polling @3s | Requests at 0, 3, 6, 9s | t=3.0s (1.0s late) | t=9.0s (2.0s late) | 2 empty responses (t=0, t=6) |
| Long-poll | 1 held request → reply @2.0s, reconnect, held → reply @7.0s | t=2.0s (~0 late) | t=7.0s (~0 late) | 0 empty, but 2 full header exchanges |
| WebSocket | 1 handshake @0, then 2 pushed frames | t=2.0s (~0 late) | t=7.0s (~0 late) | ~6 bytes framing/msg; can also send a buy order upstream instantly |
| SSE | 1 GET @0, then 2 data: events | t=2.0s (~0 late) | t=7.0s (~0 late) | Text only; auto-resumes if dropped |
Polling is the outlier: it trades latency and bandwidth for zero server-held state. The other three all deliver in one round-trip; they differ in what they cost you operationally, not in freshness.
What the wire actually looks like
SSE is just a never-ending HTTP response. The id: field is the mechanism behind its killer feature — resumption:
GET /prices HTTP/1.1
Accept: text/event-stream
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
id: 1
data: {"sym":"AAPL","px":189.20}
id: 2
data: {"sym":"AAPL","px":190.05}If the stream drops, the browser's EventSource reconnects on its own and sends Last-Event-ID: 2, so the server can replay from event 3 — no lost messages, no client code. WebSockets, by contrast, start as a one-time HTTP upgrade and then leave HTTP behind entirely:
GET /ws HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=After the 101 there is no built-in reconnect and no resumption — you own both.
Side-by-side trade-offs
| Property | Polling | Long-poll | WebSocket | SSE |
|---|---|---|---|---|
| Direction | client-pull | client-pull, delayed | full-duplex | server → client only |
| Transport | HTTP req/resp | HTTP req/resp | upgraded TCP (ws/wss) | one long HTTP response |
| Per-message overhead | full headers each poll | full headers per message | ~2–14 B frame | a few bytes per event |
| Latency | up to poll interval | ~1 RTT | ~1 RTT | ~1 RTT |
| Binary payloads | yes | yes | yes | no — UTF-8 text only |
| Auto-reconnect + resume | manual | manual | manual (you build it) | built-in (Last-Event-ID) |
| Proxy / firewall | universal | universal | can be blocked — use wss:// | universal (disable buffering) |
| Counts vs 6-connection cap | yes | yes | no (separate, higher) | yes (on HTTP/1.1) |
| Server statefulness | stateless | mostly stateless | stateful (sticky + backplane) | held stream per client; reconnect is stateless if events live in a shared, id-addressable log |
One honesty note on that last row: SSE does not exempt you from the fan-out backplane — a stream is pinned to one node just like a socket, so an event published on node B still needs a pub/sub bus to reach an SSE client held on node A. What SSE removes is sticky routing (any node can serve a resume from the shared event log keyed by Last-Event-ID) and the client-side reconnect code — not the backplane.
Pitfalls
- The 6-connection cap silently strangles SSE. Over HTTP/1.1 a browser allows only ~6 concurrent connections per origin. Each SSE stream (and each long-poll) permanently consumes one. Open your app in 7 tabs and the 7th hangs — and worse, those held connections starve ordinary page requests to the same origin. Fix: serve SSE over HTTP/2 (streams are multiplexed onto one connection), or fan a single stream out to tabs via a
SharedWorker/BroadcastChannel. WebSockets don't count against this HTTP limit. - Buffering proxies eat your stream. nginx and many CDNs buffer responses by default, so SSE/long-poll data is held until the buffer fills or the connection closes — the client sees nothing, then a burst. Fix: send
X-Accel-Buffering: no, setproxy_buffering off, and disable gzip on the stream. - Naive long-polling drops messages. Events produced in the gap between the server's response and the client's re-request vanish unless the server buffers per-client or the client passes a cursor /
Last-Event-ID. "Why the naive version is wrong": treating each long-poll as independent loses anything that happened during the reconnect window — you need a monotonic event id, exactly like SSE gives you. - WebSockets are stateful, so scaling hurts. A client pinned to server A won't receive an event published on server B unless you add a pub/sub backplane (Redis, Kafka). L7 load balancers must understand
Upgrade, and idle sockets get culled by LBs/proxies after ~60s — so you must send ping/pong heartbeats to keep them alive. - SSE reconnect storms. A server restart drops every
EventSourceat once; they all reconnect a few seconds later — the reconnection delay is browser-defined (the spec leaves it to the user agent; ~3s is typical) — simultaneously: a thundering herd. Spread it by sending per-client jitteredretry:values.
When to use which — and when not
- Choose SSE when data flows one way as text/JSON and you want reconnection + resumption for free: live feeds, notifications, dashboards, LLM token streaming, progress bars. Signals: "server pushes, client just listens." Not for high-rate client→server input or binary. Versus WebSockets, you give up the upstream channel and binary but gain built-in reliability and a simpler operating posture — most of the value of a realtime channel at a fraction of the ops cost, because you drop sticky routing, hand-rolled reconnect logic, and a heartbeat protocol (the fan-out backplane you keep either way).
- Choose WebSockets when you genuinely need bidirectional, low-latency, high-frequency traffic or binary frames: chat, collaborative editing, multiplayer games, trading terminals. Signals: both ends talk often on the same channel. Cost versus SSE: statefulness, a pub/sub backplane, sticky routing, heartbeats, proxy hostility, and reconnect logic you write yourself.
- Choose long-polling when you must support ancient clients or proxies that block WebSockets and can't do SSE, or updates are infrequent. Always prefer it over plain polling (fewer empty responses, ~1-RTT latency). Prefer WS/SSE once update rate climbs, because you pay full headers per message.
- Choose plain polling when "a few seconds late" is acceptable and you'd rather hold no connections open — e.g. a status widget refreshing every 30s. It's the cheapest server-side at massive fan-out precisely because nothing is held.
In one line: use SSE when data flows one way and you want reconnection for free; reach for WebSockets only when you truly need to talk back on the same channel; keep long-polling as the compatibility fallback; and plain polling when holding connections open isn't worth it.
Takeaways
- One axis decides everything: connection lifetime × direction of flow. Latency and cost are consequences of that choice.
- SSE = server→client + free reconnect/resume (
Last-Event-ID); WebSocket = full-duplex + binary but stateful; long-poll = universal fallback; polling = simplest, holds nothing. - The real costs are operational, not conceptual: the 6-connection cap, proxy buffering, and the WebSocket backplane + heartbeats.
- Don't reach for WebSockets by reflex — most "real-time" features are one-directional, and SSE over HTTP/2 is markedly cheaper to run.
Re-authored and deepened for this guide. Sources: MDN Web Docs (Server-sent events, EventSource, WebSockets API); RFC 6455 (The WebSocket Protocol) for the handshake key/accept example; the WHATWG HTML Living Standard (event stream format and reconnection); Ilya Grigorik, High Performance Browser Networking (transport trade-offs and the per-origin connection limit); and the original "Grokking the System Design Interview" lesson this page expands.
🤖 Don't fully get this? Learn it with Claude
Stuck on LongPolling vs WebSockets vs ServerSent Events? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **LongPolling vs WebSockets vs ServerSent Events** (System Design) and want to truly understand it. Explain LongPolling vs WebSockets vs ServerSent Events from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **LongPolling vs WebSockets vs ServerSent Events** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **LongPolling vs WebSockets vs ServerSent Events** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **LongPolling vs WebSockets vs ServerSent Events** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.