Polling vs WebSockets vs SSE

A decision framework comparing short polling, long polling, Server-Sent Events, and WebSockets.

Four ways to get "real-time-feeling" updates

Every approach to getting fresh data to a browser without a full page reload boils down to one of four patterns. Picking the right one is a genuine trade-off decision, not a matter of always reaching for the newest-sounding option.

Short polling

The simplest possible approach: the client just asks "anything new?" on a fixed interval, using a completely ordinary fetch() call each time.

Javascript
setInterval(async () => {
  const response = await fetch('/api/notifications/unread-count');
  const data = await response.json();
  updateBadge(data.count);
}, 5000);

Simple to build and reason about, works through any proxy or firewall that handles plain HTTP, and needs no special server support at all. The costs: most requests return "nothing changed," wasting bandwidth and server work, and freshness is capped by the interval — a change that happens right after a poll won't be seen for up to 5 seconds in the example above.

Long polling

A refinement: the client sends a request, but instead of the server replying immediately with "nothing yet," it holds the connection open until either new data becomes available or a timeout is reached — and the client immediately re-requests the moment it gets a response.

Javascript
async function pollForUpdates() {
  try {
    const response = await fetch('/api/notifications/wait-for-update'); // server holds this open
    const data = await response.json();
    handleUpdate(data);
  } catch (error) {
    console.error('Long-poll request failed:', error);
  } finally {
    pollForUpdates(); // immediately re-issue, whether it succeeded, timed out, or failed
  }
}

pollForUpdates();

This gets close to real-time delivery (the server responds the instant something happens, not on the next fixed tick) while still being built entirely on ordinary HTTP request/response — no upgrade handshake, no new protocol, works through virtually any infrastructure. The cost is a server that now has to hold open many idle connections simultaneously, one per waiting client, which doesn't scale as cleanly as a stateless request/response API.

Server-Sent Events (SSE)

SSE gives the server a way to push a one-way stream of updates to the client over a single long-lived HTTP connection, using the browser's built-in EventSource API:

Javascript
const events = new EventSource('/api/notifications/stream');

events.onmessage = (event) => {
  const notification = JSON.parse(event.data);
  showNotification(notification);
};

events.onerror = (error) => {
  console.error('SSE connection error:', error);
  // EventSource reconnects automatically — no manual retry logic needed
};

The server keeps the response stream open and writes each event as plain text in a simple format, flushing after every one:

Text
data: {"type":"new_message","from":"Ali"}

data: {"type":"friend_request","from":"Sam"}

The two blank-line-separated data: blocks above are each delivered as one message event on the client. EventSource handles automatic reconnection with a retry delay if the connection drops — a feature the raw WebSocket API does not provide out of the box. The trade-off is that SSE is strictly one-directional: the client can't send anything back over that same connection, so any client-to-server communication needs a separate ordinary fetch() call alongside it.

WebSockets

Covered in full in this app's dedicated WebSockets track: a persistent, full-duplex connection where either side can send a message at any time. The right tool specifically when the client also needs to send frequent messages back over the same live connection — chat, multiplayer game state, collaborative editing — not just receive pushed updates.

Decision framework

Short polling Long polling SSE WebSockets
Direction Client pulls Client pulls (delayed until data's ready) Server pushes only Both directions
Transport Plain HTTP, one request per interval Plain HTTP, held open Plain HTTP, one long-lived stream Its own protocol, after an HTTP upgrade
Typical latency Up to the poll interval Near-immediate Near-immediate Near-immediate
Server connections held open None between polls One per waiting client One per connected client One per connected client
Built-in reconnection N/A — a fresh request every time Client's loop just re-issues the request Yes, automatic in EventSource No — must be hand-rolled
Client can send data back on the same channel No No No Yes
Infra/proxy friendliness Universal Universal Good, though some older proxies buffer long-lived streams Usually fine, but some restrictive corporate proxies block the upgrade

As a decision framework:

  • Need frequent, low-latency messages flowing both ways? Use WebSockets.
  • Only need the server to push updates, with no client-to-server messages on the same channel? Use SSE — simpler than WebSockets, works over plain HTTP, and reconnects automatically for free.
  • Updates are infrequent and a few seconds of staleness is genuinely fine? Short polling, with no extra infrastructure investment, is a perfectly reasonable choice — not everything needs to feel instant.
  • Need lower latency than short polling can offer, but can't take on WebSocket/SSE infrastructure yet, or need to support very old or restrictive network paths? Long polling is a reasonable middle ground, built entirely on plain HTTP.

Common mistakes

  • Reaching for WebSockets by default for a feature that's actually one-directional — SSE solves the same problem with less complexity and automatic reconnection built in.
  • Setting a short-polling interval aggressively low (every few hundred milliseconds) in the name of "real time," when the actual server load and latency profile of long polling or SSE would be strictly better.
  • Forgetting that SSE is one-way only, and trying to send data back over the same EventSource connection — it can't; pair it with an ordinary fetch() call for the send direction.
  • Not accounting for infrastructure that buffers or kills long-lived HTTP connections (some corporate proxies, some older load balancer defaults) when choosing long polling or SSE — this is exactly the kind of failure that looks like a client bug but is actually a network path issue.