Real-time features — live dashboards, chat, notifications, collaborative editing — all face the same fundamental question: how does fresh data get from the server to the browser? Over the years, developers have settled on three main answers: polling, Server-Sent Events (SSE), and WebSockets. They look similar from a distance but differ sharply in directionality, complexity, and infrastructure demands. Pick the wrong one and you'll pay in wasted requests, fragile proxies, or over-engineered servers.
This article explains how each mechanism actually works, then gives you a decision framework for choosing between them.
Polling: The Simplest Tool
Polling is HTTP's most basic trick for “real-time” updates: the client asks the server for new data, on a schedule, whether or not anything changed. There are two flavours.
Short polling is exactly what it sounds like. The client issues a request every N seconds — “any new messages?” — and the server answers immediately, every time, even when the answer is “no.” The cost is obvious: most requests return nothing, and your data is always up to N seconds stale. It is crude, but it works everywhere HTTP works, through any proxy, firewall, or CDN, with zero special server support.
Long polling is the cleverer cousin. The client sends a request, and the server simply doesn't answer — it holds the request open until new data arrives or a timeout is reached (commonly around 60 seconds), then responds. The client processes the response and immediately opens a new request. You get near-real-time latency over plain HTTP, at the cost of one held-open connection and a server-side handler per waiting client. Your server must be able to keep thousands of idle connections parked without burning threads — event-driven servers (Node.js, nginx, async Python) handle this naturally; a thread-per-request server will suffer.
Polling's virtue is universality. It works through every proxy and firewall, in every browser, on every network — including the hostile corporate ones. Its vice is efficiency: short polling wastes requests, and long polling ties up server resources in direct proportion to your connected user count.
Server-Sent Events: One-Way Streaming Over Plain HTTP
Server-Sent Events, or SSE, is a browser-native standard defined in the WHATWG HTML Standard — not an IETF RFC. It gives you a one-way stream from server to client over ordinary HTTP. The server responds with the MIME type text/event-stream, the content encoded as UTF-8, and then keeps writing event blocks as they happen.
On the client side, the API is refreshingly simple:
const events = new EventSource('/events');events.onmessage = (event) => { console.log(event.data);};// named events:events.addEventListener('price-update', (event) => { handlePrice(event.data);});Each event is plain text: data: ... lines, optionally preceded by an event: name and an id:. That id: field is the clever part. If the connection drops, the browser reconnects automatically and sends a Last-Event-ID HTTP header containing the last event ID it received — so your server can resume the stream or replay missed events. The server can also control the retry delay with a retry: field (typically around 3 seconds by default). Automatic reconnection with resume is built into the protocol; you don't write a single line of it.
SSE is one-way and text-only: there is no binary support, and if the client needs to talk back, it does so with ordinary HTTP requests (a fetch() POST, for instance). That sounds like a limitation, but for a huge class of use cases — live sports scores, stock tickers, notifications, system-monitoring dashboards, news feeds, and streaming LLM tokens — the data flow genuinely is one-directional, and SSE is the leanest tool that fits.
SSE has one historical gotcha worth knowing: under HTTP/1.1, browsers cap concurrent connections to about 6 per host, and each open SSE stream counts against that limit. Six open streams can stall all other requests to your domain — images, stylesheets, XHRs — until a slot frees up. HTTP/2 multiplexing removes this bottleneck entirely, letting many logical streams share a single TCP connection, so make sure your SSE endpoints are served over HTTP/2 (or 3). Because SSE is plain HTTP, it also passes through proxies and CDNs with no special handling — unlike WebSockets, which some deep-inspection firewalls struggle with.
WebSockets: Full-Duplex Over One Connection
WebSockets, standardized as RFC 6455 in December 2011 (authors Fette and Melnikov), take a different approach. They begin life as an ordinary HTTP request — the client sends an Upgrade: websocket header — and if the server agrees, it answers HTTP/1.1 101 Switching Protocols. Any other status code means the handshake failed. From that point on, the HTTP protocol is gone: what remains is a persistent, full-duplex channel over a single TCP connection, where each side can send data at will, independently of the other.
Data travels in frames — text frames (interpreted as UTF-8), binary frames (interpreted by the application however it likes), and control frames like close and ping/pong. On the client side:
const socket = new WebSocket('wss://example.com/chat');socket.onmessage = (event) => { console.log(event.data);};socket.send('hello'); // send anytime, in either directionThis bidirectionality is the whole point. Chat applications, multiplayer games, collaborative editors, live whiteboards, and trading platforms all need low-latency, two-way communication — and WebSockets deliver it with minimal framing overhead after a single handshake.
The tradeoff is operational. WebSockets have no built-in reconnection: a dropped connection is yours to detect and re-establish, typically with exponential backoff and a resync handshake of your own design. Load balancers and proxies must be configured to handle long-lived connections (sticky sessions, idle timeouts). And while wss:// on port 443 looks like ordinary HTTPS to most network middleboxes, some enterprise firewalls with deep packet inspection still break the Upgrade — which is why you should always have a fallback story.
Head-to-Head Comparison
- Direction: Polling and SSE are client-initiated; SSE streams server→client only. WebSockets are fully bidirectional.
- Latency: WebSockets and SSE are push — data arrives the moment it's ready. Long polling is near-push. Short polling lags by up to the poll interval.
- Protocol: SSE and polling are plain HTTP(S) and pass through proxies, CDNs, and firewalls transparently. WebSockets need the HTTP Upgrade and can trip up aggressive middleboxes.
- Reconnect logic: SSE reconnects and resumes automatically via
Last-Event-ID. WebSockets leave reconnection entirely to you. Polling, by definition, reconnects every cycle. - Payloads: WebSockets carry binary or text. SSE is text-only. Polling carries whatever HTTP carries.
- Server cost: Short polling costs requests; long polling and SSE cost held connections; WebSockets cost held connections plus state. All three demand an event-driven server at scale.
- Browser support:
EventSourceandWebSocketare universal in modern browsers; polling works literally everywhere.
Choosing: A Decision Framework
Start with the data flow:
- Does the client need to send data in real time? If both sides talk — chat, games, collaborative editing, live whiteboards — reach for WebSockets. SSE can't do this; faking it with POSTs alongside SSE is fine for occasional input but clumsy for true duplex interaction.
- Is the data flow one-way? Live scores, price tickers, build notifications, monitoring dashboards, AI token streaming — SSE is almost always the right call. You get push semantics, automatic reconnection, plain-HTTP infrastructure, and a five-line client, with none of WebSockets' operational overhead. (This covers, frankly, more real-time features than most teams realize.)
- Are updates infrequent or the network hostile? If freshness on the order of seconds is fine, or you're deploying into corporate networks and legacy environments you don't control, polling — short for simplicity, long for efficiency — is the tool that cannot be blocked. It is also the right answer when your team doesn't yet need true push and would rather ship than operate.
A few finer points that often decide the matter:
- Binary data (audio chunks, protobuf payloads) rules out SSE — use WebSockets or polling.
- HTTP/1.1-only hosting with multiple simultaneous SSE streams will hit the ~6-connection-per-host cap; serve over HTTP/2 or consolidate streams.
- CDN caching works naturally with polling and (carefully) with SSE endpoints, but not with WebSocket connections.
- Scaling: whichever push option you choose, plan for connection fan-out early — a single app server holds a finite number of idle connections, and you'll eventually want a pub/sub layer (Redis, NATS, or a managed service) behind your edge.
The pragmatic default for most teams: SSE for one-way feeds, WebSockets for true duplex, polling as the universal fallback. Resist the urge to default to WebSockets for everything — they're the most powerful option and the most expensive to operate. And when someone proposes polling for a live feed, make sure they've priced the wasted requests before dismissing it; sometimes “boring and unbreakable” is exactly the right engineering call.
Further reading
- RFC 6455 — The WebSocket Protocol (IETF, December 2011): the authoritative spec, including the handshake and framing details.
- WHATWG HTML Standard — Server-Sent Events: the normative definition of
EventSource, the event-stream format, andLast-Event-IDresume. - MDN — Using server-sent events: practical client and server examples.
- Ably — WebSockets vs SSE: a thorough comparison with real-world tradeoff analysis.
