Real-time Updates — Client Pulling
Short polling, long polling, and when pulling beats pushing.
The simplest family of real-time is client pull: if the server can't knock, the client keeps asking. Two variants — short polling and long polling — trade wasted requests against latency. Both are workarounds for the same HTTP limit.
Short vs. long polling
Watch both run live. Short polling fires on a fixed interval — most requests come back empty (wasted), and latency is capped by the interval. Long polling lets the server hold the request open and answer the instant data appears — almost no waste, near-zero latency. Toggle, or flip to Autoplay:
Short polling, in code
The naive version is about ten lines: ask on a timer, render whatever comes back, repeat.
useEffect(() => {
const id = setInterval(async () => {
const res = await fetch("/api/messages");
setMessages(await res.json());
}, 5000);
return () => clearInterval(id);
}, []);The latency here equals your interval: a 5s interval means updates can be up to 5s stale. Shrink the interval and latency drops — but server load climbs. That tension is the whole game.
Four pitfalls of naive polling
That ten-line version breaks in real apps in at least four ways — here's each one as a picture.
1 · Race conditions
Poll takes 3s, interval is 2s → two requests in flight, and responses arrive out of order:
setTimeout recursion (never overlap), or cancel the in-flight request with AbortController before the next one.2 · Polling on a hidden tab
The user switches tabs, but your interval keeps hammering the server for nothing — and drains mobile battery:
3 · No backoff on errors
The server is down, so the naive version keeps hitting it at the same rate, making recovery harder:
4 · Re-rendering identical data
The response hasn't changed, but you re-render the whole list anyway:
Fix all four together and the “ten lines” grow into a small but robust poller — sequential recursion (no overlap), visibility pausing, backoff with jitter, and a since-cursor:
function createPoller({ url, interval = 5000, onData }) {
let stopped = false, failures = 0, lastSeen = null, controller;
const tick = async () => {
if (stopped) return;
if (document.hidden) return schedule(interval); // pitfall 2
controller = new AbortController(); // pitfall 1
try {
const res = await fetch(`${url}?since=${lastSeen ?? ""}`,
{ signal: controller.signal }); // pitfall 4
const data = await res.json();
if (data.length) { lastSeen = data.at(-1).id; onData(data); }
failures = 0; schedule(interval);
} catch { // pitfall 3
const wait = Math.min(60000, interval * 2 ** ++failures);
schedule(wait * (0.8 + Math.random() * 0.4)); // backoff + jitter
}
};
const schedule = (ms) => { if (!stopped) setTimeout(tick, ms); };
tick();
return () => { stopped = true; controller?.abort(); };
}refetchInterval that handles visibility, focus, backoff, and dedup for free. But knowing the failure modes is what an interview tests.When short polling wins
Despite the trendy dismissals, short polling is genuinely the right call a lot of the time. Tap each:
Long polling: hold the line
Long polling fixes short polling's biggest flaw — wasted requests. The client sends a request; instead of answering “nothing yet,” the server holds it open until either new data arrives (answer immediately) or a timeout hits (answer empty). The client then re-sends right away.
Stateless vs. stateful
Short polling is stateless: each request stands alone, so any server can answer and scaling is trivial. Long polling is stateful — a held connection lives on one machine. The canonical failure is a file upload: it's at 55% when that machine dies, and the next server has no idea.
Why that matters comes down to how you scale:
− Hard ceiling · single point of failure · cost grows non-linearly.
+ No ceiling · survives failures · linear cost — but needs statelessness.
So to keep long polling working across many servers, you need one of two fixes:
The cheat sheet
| Short polling | Long polling | |
|---|---|---|
| Latency | = interval + round-trip | ≈ server processing (near-zero) |
| Wasted requests | Many empty polls | ~none |
| Server state | Stateless — trivial to scale | Stateful — holds open connections |
| Complexity | Dead simple | More moving parts (state, balancing) |
| Best when | Infrequent updates; staleness OK | Lower latency without a new protocol |
Check your understanding
Design challenge
Design polling for a “new notifications” badge:
- Short or long polling — and why, given the staleness window?
- Sketch how you'd add backoff, visibility-pausing, and a since-cursor.
- If you chose long polling, where does the held state live so a server crash doesn't lose it?
- →Client pull works around HTTP by having the client keep asking.
- →Short polling: fixed interval, simple and stateless, but wasted requests and latency = interval.
- →Naive polling needs race-condition handling, visibility pausing, backoff + jitter, and since-cursors.
- →Long polling: server holds the request → near-zero latency and waste, but it's stateful (sticky sessions or a shared store).
- →Short polling is a great fallback — even WebSockets fall back to it.