πŸ”„ Polling vs Push

Four points on one spectrum β€” short polling, long polling, SSE, and WebSockets β€” trading network overhead and complexity for lower latency.

The Spectrum

"How does the client learn about new data?" has four common answers. They form a ladder: each rung cuts latency and wasted requests, and adds a bit of complexity or infrastructure demand.

flowchart LR A["Short polling
ask every N seconds"] --> B["Long polling
ask, server holds
until data"] B --> C["SSE
one open response,
server streams"] C --> D["WebSockets
persistent full-duplex"] A -. more overhead / higher latency .-> D

Short Polling

The client asks on a fixed interval. Dead simple, works everywhere, and usually wrong for realtime.

setInterval(async () => {
  const r = await fetch("/api/messages?since=" + lastId);
  const items = await r.json();
  if (items.length) render(items);
}, 3000);   // up to 3s stale, and a request every 3s even when nothing changed
The waste: pick a 1s interval and 3600 requests/hour/client hit your server even when nothing happens. Pick 30s and updates lag by up to 30s. There's no interval that's both cheap and fresh.
It's not always wrong. For data that changes slowly and tolerates staleness (a build status, a daily counter), short polling is the least code and the least to operate. Don't over-engineer.

Long Polling

The client makes a request; the server holds it open until data is available (or a timeout), then responds. The client immediately re-requests. Latency approaches push, using only plain HTTP.

async function longPoll() {
  try {
    // server blocks up to ~30s waiting for new data, then returns
    const r = await fetch("/api/updates?since=" + lastId, { signal: timeout(35000) });
    if (r.status === 200) { const d = await r.json(); lastId = d.id; render(d); }
  } catch (_) { /* timeout β€” just loop */ }
  longPoll();   // reconnect immediately
}
longPoll();
Long polling is push emulated over request/response. It's the fallback every realtime library ships for environments where SSE/WebSockets are blocked. The cost is a held connection per client and a reconnect on every message.

SSE and WebSockets

The top two rungs keep a single connection open and stream over it β€” no per-message reconnect. SSE streams one-way over HTTP with built-in reconnect; WebSockets add a return channel. Full detail on their own pages.

Rule of thumb: server→client only ⇒ SSE; both directions, chatty ⇒ WebSockets. Reach down to long polling only as a compatibility fallback.

The Tradeoff Table

Short pollLong pollSSEWebSockets
LatencyUp to interval~Instant~Instant~Instant
Wasted requestsHighLowNoneNone
DirectionPullPull (emulated push)Server pushFull-duplex
Connections heldNone (bursty)One per clientOne per clientOne per client
Client complexityTrivialLowLowHigher
Server modelStatelessHeld requestsStreaming responsesStateful sockets
Reconnect/resumeN/AManualBuilt inManual

Choosing

flowchart TD Q1{"Need realtime
at all?"} -->|"No / slow-changing"| SP["Short polling"] Q1 -->|"Yes"| Q2{"Does the client
also send often?"} Q2 -->|"Yes"| WS["WebSockets"] Q2 -->|"No β€” server pushes"| Q3{"SSE/streaming
allowed by infra?"} Q3 -->|"Yes"| SSE["SSE"] Q3 -->|"No (locked-down proxy)"| LP["Long polling fallback"]
Start low, climb only when forced. Most teams jump straight to WebSockets and inherit reconnection, backpressure, heartbeats, and stateful fan-out. Reach for the highest rung only when the one below genuinely can't meet the requirement.

Key Takeaways