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.
See the difference
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:
🖥
Client
🗄
Server
Requests sent
0
Wasted (empty)
0
Fixed interval · latency = the interval · lots of empty polls.
Variant 1
Short polling, in code
The naive version is about ten lines: ask on a timer, render whatever comes back, repeat.
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.
Production reality
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:
ClientServer
Request #4
▶
slow · 3s
Request #5
▶
fast · 1s
Response #5
◀
arrives FIRST · ✓ fresh
Response #4
◀
arrives LAST · ✕ stale
The UI renders #4 on top of #5 → the stale response wins.
Fix: sequential 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:
● tab visible
GmailYour appDocs
Your app is in front — document.hidden === false. Polling is useful.
app
server
Wasted polls 0
Battery100%
The fix
Pause when document.hidden is true via the Page Visibility API, and resume on visibilitychange. Flip the toggle to feel it.
3 · No backoff on errors
The server is down, so the naive version keeps hitting it at the same rate, making recovery harder:
● 503 · DOWN
3 of 10,000 clients · all retry in sync
✕
Thundering herd: synchronized retries keep pounding the dead server — it never gets room to recover.
4 · Re-rendering identical data
The response hasn't changed, but you re-render the whole list anyway:
200 · full re-render
Wasted re-renders
0
flicker · scroll jumps
Identical data, but the whole list re-mounts every poll — wasted bandwidth, flicker, lost scroll position.
The fix
Send ?since=<lastId> so the server returns 304 or [] when nothing's new. Use an ETag, or diff before touching the DOM.
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:
You rarely hand-roll all this — TanStack Query and SWR ship a refetchInterval that handles visibility, focus, backoff, and dedup for free. But knowing the failure modes is what an interview tests.
Don't underestimate it
When short polling wins
Despite the trendy dismissals, short polling is genuinely the right call a lot of the time. Tap each:
The one limit
Short polling's floor is the interval: latency can never beat it. Need lower latency without that floor? That's exactly what long polling buys you.
Variant 2
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.
What you gain, what you pay
Latency drops to roughly server-processing time (no interval wait), and wasted requests fall to near zero. The price: the server now holds state — an open connection per client — which complicates scaling.
The hidden cost
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.
First request
User
→
LB
→
Server A
Upload 55% — Server A holds the state.
Reconnect after the connection drops
User
→
LB
→
Server B
Upload 0% — Server B has no idea!
While it holds the request, the server has context the database doesn't — the subscription, the timeout, the partial progress. Lose that server and you lose all of it.
Why that matters comes down to how you scale:
Vertical · scale UP
→BIG
One machine — more CPU / RAM / disk. − Hard ceiling · single point of failure · cost grows non-linearly.
Horizontal · scale OUT
+ more…
Many interchangeable machines. + No ceiling · survives failures · linear cost — but needs statelessness.
Horizontal scaling needs interchangeable, stateless servers so a request can land anywhere. A held long-poll connection carries state — which is exactly what breaks it.
So to keep long polling working across many servers, you need one of two fixes:
Client 1
Client 2
LB
Server A
Server B
Send a request to see how the load balancer routes it.
Pros
+ Easy — most LBs support it out of the box
+ No new infrastructure to run
Cons
− Uneven load — hot clients pile up
− Server dies → its state is still lost
− DDoS amplification risk
Best for short-lived, non-critical state (file uploads). It still loses in-flight state on a crash.
Bridge to the backend
A choice that's “10 lines” on the frontend can be a major backend complication. Strong frontend engineers understand the other side well enough to weigh that trade-off — not just pick the tool.
Side by side
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
Tip
Don't dismiss short polling as primitive — it's simple, adds no load when used appropriately, and is a perfect fallback. WebSockets themselves fall back to long polling when a connection fails.
Practice
Check your understanding
Q1Multiple choice
Your poll takes ~3s but you setInterval every 2s. Users occasionally see an older message list flash over a newer one. Root cause and the right fix?
Q2Multiple choice
You move from short to long polling and add a second server behind a load balancer. Held connections now 'lose their place' after a deploy. Why — and what does long polling need that short polling didn't?
Q3Multiple choice
A reviewer says 'short polling is primitive — always prefer long polling or WebSockets.' When is short polling genuinely the better call?
Q4Sort each scenario
Which fix addresses each polling pitfall?
A failing endpoint being hammered every interval (and all clients retrying in lockstep)
Polling continuing in a hidden background tab, draining battery
Re-rendering the same unchanged list every tick
Two slow polls in flight, responses landing out of order
Try it yourself
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?
Key takeaways
→Client pull works around HTTP by having the client keep asking.
→Short polling: fixed interval, simple and stateless, but wasted requests and latency = interval.