Short polling
The browser asks the server "anything new?" on a timer: the simplest way to keep a page fresh, and the right answer more often than people think.
The problem: the server can't speak first
You order dinner from a food delivery app. The order page says "Restaurant is preparing your food". Twenty minutes later the driver is at your door, and the page still says "preparing". It only changes when you pull down to refresh.
Nothing is broken. The page loaded once, showed what the server said at that moment, and then stopped. That is how plain HTTP works: the browser asks, the server answers, and the conversation ends. The server cannot send a message to the browser on its own, because there is no open request for it to answer.
Many pages need to change while the user looks at them: an order status, a build log, a payment that is being confirmed, a count of unread messages, a file export that takes a minute. Each one needs some way for news on the server to reach a page that is already open.
Four ways to get news to the browser
There are four common techniques, and this module covers each of them. They differ in who starts the conversation and how long the connection stays open.
- Short polling. The browser sends a normal request every few seconds and asks "anything new?". The server answers at once, even if the answer is "no".
- Long polling. The browser asks, and the server holds the request open until it has news (or a timeout passes), then answers. The browser asks again straight away.
- Server-sent events (SSE). The browser opens one HTTP response that never ends, and the server writes events into it as they happen.
- WebSockets. The browser upgrades a connection to a two-way channel, and either side can send a message at any time.
Short polling is the oldest and the simplest. It needs no special server, no long-lived connections, no new protocol, and no change to your load balancers, proxies, or CDN. Any endpoint that returns JSON can be polled. That is why it is still everywhere: in payment pages, CI dashboards, admin panels, and the status pages of large APIs.
What it costs
The price is a trade-off you set with one number, the interval (the wait between two requests). Ask often and most answers say "nothing new": wasted requests. Ask rarely and news reaches the user late. No interval gives you both, so a good polling design is mostly about two things: asking less often when it does not matter, and making each "nothing new" answer as cheap as possible.
This lesson takes you through both. You will size the load with real numbers, write a polling loop that does not pile up requests, slow down when nothing changes, stop in hidden tabs, use ETag and 304 to make empty answers almost free, spread clients out with jitter, and let the server tell clients when to come back. At the end you will know when polling is the right choice and how to defend it in an interview.
Check your understanding · Exercise 1
Why can't a plain HTTP server push a status change to a page that is already open? Pick one.
The mental model: delay against requests
Think of a child in the back seat asking "are we there yet?". The driver answers every time, even when nothing has changed. Ask every minute and the child learns quickly that you have arrived, but the driver says "no" sixty times an hour. Ask every half hour and the driver gets peace, but the child may sit in the parked car for 29 minutes before finding out.
Short polling is exactly that. Two numbers matter, and the interval sets both of them: how late the user sees a change (the delay), and how many requests your servers answer (the load). You can't improve one without paying in the other, so you need to be able to work out both quickly.
How late does a change arrive?
A change on the server happens at a random moment between two polls. If it lands just before a poll, the user sees it almost at once. If it lands just after one, it waits a whole interval. On average it waits half an interval, plus the time the request itself takes.
So a 10-second interval with 200 ms responses means a typical delay of about 5.2 seconds and a worst case of about 10.2 seconds. That is fine for an order status and far too slow for a chat message. Always quote both numbers: the average sets how the feature feels, the worst case sets what users complain about.
How many requests does it cost?
The load formula is one line: requests per second = users with the page open divided by the interval in seconds. Count users who have the page open at the same moment (concurrent users), not daily users.
| Open pages | Interval | Requests per second | Requests per day |
|---|---|---|---|
| 1 | 10 s | 0.1 | 8,640 |
| 5,000 | 10 s | 500 | 43 million |
| 20,000 | 10 s | 2,000 | 173 million |
| 20,000 | 2 s | 10,000 | 864 million |
| 20,000 | 60 s | 333 | 29 million |
Now compare that with how often the data really changes. If an order status changes 6 times in a 40-minute delivery and the page polls every 10 seconds, that is 240 polls for 6 changes: more than 97% of answers say "nothing new". This ratio, useful answers to total answers, is the number that tells you whether polling is wasteful for a feature.
Requests are not free even when the answer is empty. Each one carries request headers and cookies (often 500 bytes to 2 KB), passes through your load balancer, authenticates the user, and usually reads a database or a cache. On a phone, each request also wakes the cellular radio, which then stays in a high-power state for several seconds, so frequent polling drains the battery even when almost no bytes move.
20,000 pages every 10 s is 2,000 requests per second before savings. A change waits 5 s on average and 10 s at worst. 97.5% of answers say "nothing new".
There is no magic interval
Every fixed interval is a compromise, so stop looking for the perfect one. Pick a starting interval from the delay the product can live with, then use the techniques in the rest of this lesson to cut the load without making active users wait: slow down when nothing changes, stop when nobody is looking, make empty answers cheap, and let the server steer.
Check your understanding · Exercise 2
50,000 users have a dashboard open and it polls every 5 seconds. Which statements are true? Select all that apply.
Writing the loop: setTimeout chains, not setInterval
The polling loop has four steps: send a request, use the answer, wait, repeat. The obvious way to write it is setInterval(check, 5000). It is also the most common polling bug.
What goes wrong with setInterval
setInterval fires on the clock, every 5 seconds, whether or not the last request has finished. On a good network that does not matter: each request takes 150 ms and is long done before the next one starts. On a slow train connection, or when the server is overloaded and answers take 7 seconds, the requests start to overlap.
- They pile up. A new request starts every 5 seconds but each takes 7, so more and more are in flight at once. An overloaded server gets even more traffic from the very clients it is failing to serve, which makes it slower still.
- They finish out of order. Request 2 may hit a slow database replica and request 3 a fast one. If request 3 answers first with "Out for delivery" and request 2 answers later with the older "Preparing", the page goes backwards.
- They waste connection slots. Over HTTP/1.1 a browser opens at most 6 connections to one host. Several stuck polls can make the page's other requests wait in line.
setInterval, a slow server gets more requests at once, and they can answer out of order. The chain keeps one request in flight and slows down with the server.The fix: schedule the next poll after the answer
A setTimeout chain waits for each answer, then schedules the next request. There is only ever one request in flight, so nothing piles up and nothing arrives out of order. When the server slows down, the clients slow down with it, which is exactly what an overloaded server needs.
The chain has a second benefit: the wait can change on every turn. That is what makes adaptive intervals, backoff, and server hints (later chapters) easy to add. With setInterval you would have to clear and recreate the timer each time.
async function poll(signal: AbortSignal) {
let wait = 5000;
while (!signal.aborted) {
try {
const res = await fetch("/api/orders/481/status", {
signal: AbortSignal.any([signal, AbortSignal.timeout(8000)]),
});
if (res.ok) {
const data = await res.json();
render(data);
if (data.status === "delivered") return; // final state: stop
}
} catch {
if (signal.aborted) return; // stopped on purpose
}
await sleep(wait, signal);
}
}
const controller = new AbortController();
poll(controller.signal); // controller.abort() on unmount stops it
Four details that matter
- A time limit on every request. Without one, a request that hangs for minutes freezes the loop.
AbortSignal.timeout(8000)cancels it after 8 seconds, andAbortSignal.anycombines it with the signal that stops the whole loop. - A clean stop. In a React component, start the loop in an effect and call
controller.abort()in its cleanup. Forgetting this leaves loops running after the user navigates away, and every visit to the page adds one more. - A final state. Most polled things end: the order is delivered, the build passed, the export is ready. Stop polling there. A loop that keeps asking about a finished job is pure waste.
- Skip work when nothing changed. Compare a version number or
updatedAtwith the last answer and skip rendering if they match. This keeps the UI calm and avoids losing the user's scroll position or text selection.
If you use a data library, it already does most of this. TanStack Query takes a refetchInterval option (a number, or a function that returns the next interval or false to stop) and does not poll hidden tabs unless you set refetchIntervalInBackground. SWR has refreshInterval and only polls hidden tabs if refreshWhenHidden is on. Know what they do so you can explain it, but don't hand-roll a loop when the app already uses one of them.
Check your understanding · Exercise 3
A page uses setInterval to poll every 3 seconds. The server is under load and answers take 9 seconds. What happens? Select all that apply.
Adaptive intervals and backoff
A fixed interval treats every moment the same, but real data has rhythms. A delivery status hardly moves while the restaurant cooks, then changes often once the driver is close. A build log changes every second while the build runs, and never again after it ends. A fixed interval is too fast for the quiet parts and too slow for the busy parts.
Adaptive intervals: follow what is happening
An adaptive interval changes the wait based on what the last answers said. The usual rules are short enough to remember:
- Start short, say 2 seconds, because the user just arrived and is paying attention.
- Each time an answer says "nothing new", wait longer: multiply the wait by 1.5 or 2, up to a cap such as 30 or 60 seconds.
- When an answer brings a change, or the user clicks, types, or focuses the window, drop back to the shortest wait. Activity predicts more activity.
- When the thing reaches a final state, stop.
The effect on load is large. A page that polls every 2 seconds sends 1,800 requests an hour. With waits of 2, 4, 8, 16, then 30 seconds for the rest of a quiet hour, it sends about 125. Active moments still feel instant, because any change resets the wait to 2 seconds.
Adaptive 2 to 30 s: 38 polls in 10 minutes, 5 with news. A change waited 9.0 s on average and 22 s at most.
Backoff: when the server is in trouble
Adaptive intervals react to the data. Backoff reacts to errors. When a poll fails with a network error, a timeout, or a 5xx status, polling again at the normal rate is the worst thing to do: if the server is down because it is overloaded, thousands of clients retrying every 2 seconds keep it down.
Exponential backoff doubles the wait after each failure in a row: 2, 4, 8, 16, 32 seconds, up to a cap like 2 minutes. The first success resets it to normal. Add randomness to each wait (the jitter chapter explains why), so that clients that failed together don't all come back together.
const MIN = 2_000;
const MAX = 30_000;
const ERROR_MAX = 120_000;
function nextWait(prev: number, outcome: "changed" | "same" | "error") {
if (outcome === "changed") return MIN;
if (outcome === "same") return Math.min(prev * 2, MAX);
return Math.min(prev * 2, ERROR_MAX); // back off harder on errors
}
// user activity also resets the wait
window.addEventListener("focus", () => { wait = MIN; wakeNow(); });
Tell the user when data may be old
When polls keep failing, the page still shows the last good data, and the user can't tell it is stale. Show a quiet note such as "Last updated 2 minutes ago" with a retry button, and put it in a polite live region (aria-live="polite") so screen reader users hear it without being interrupted. Don't announce every successful poll: a screen reader that says "updated" every 5 seconds is unusable.
Not every error deserves backoff. A 401 means the session expired: polling again will never work, so stop and send the user to sign in. A 404 means the thing was deleted: stop and say so. Backoff is for errors that might go away on their own: network failures, timeouts, 429, 502, 503, and 504.
Check your understanding · Exercise 4
A poll starts failing with 503 Service Unavailable. Which reactions are right? Select all that apply.
Making "nothing new" cheap: ETag, 304, and caches
Even after pausing hidden tabs and slowing down quiet pages, most polls still find nothing. So the next goal is to make the empty answer as cheap as possible for the network, the server, and the browser. HTTP already has the tool for this: conditional requests.
How ETag and If-None-Match work
- The server labels each version of the data with an ETag (entity tag), a short string in quotes such as
"v42", sent in theETagresponse header. - The client keeps that tag with the data. On the next poll it sends it back:
If-None-Match: "v42". - If the data is still version 42, the server answers
304 Not Modifiedwith no body. The client keeps what it has. - If the data changed, the server answers
200with the new body and a new tag,ETag: "v43".
If-None-Match. While the data is unchanged, a poll costs about 250 bytes of headers instead of 40 KB.What a 304 saves, and what it does not
A 304 still costs a full round trip, so it does not make updates faster. What it removes is the body. For a 40 KB order list, each empty poll drops from about 40 KB plus headers to a few hundred bytes of headers. At 2,000 polls per second that is about 80 MB per second of JSON that is never sent, never parsed, and never re-rendered.
The server saves real work only if it can check the version without building the response. Computing the ETag by hashing the full JSON still means loading everything from the database first: you save bandwidth but not CPU. Better is a version number or an updated_at timestamp on the record, which the server can read with one cheap query or from a cache such as Redis, and answer 304 before touching anything else.
Two details that trip people up
- fetch and 304. When your code sets
If-None-Matchitself, the browser's HTTP cache steps aside andfetchhands you the raw304with an empty body. Your code must keep the last data and reuse it. The alternative is to let the browser do it: sendCache-Control: no-cachewith anETag, and the browser revalidates on its own and gives your code a normal200from its cache. That is simpler, but you can no longer tell "changed" from "not changed" by the status. - Cross-origin APIs.
If-None-Matchis not on the list of headers allowed without a CORS preflight, so polling another origin with it adds anOPTIONSrequest first. SendAccess-Control-Max-Ageso the browser caches the preflight (Chrome caps that at 2 hours). The server must also listETaginAccess-Control-Expose-Headers, or your code can't read the tag.
let etag: string | null = null;
let last: OrderList | null = null;
async function check(signal: AbortSignal) {
const res = await fetch("/api/orders", {
headers: etag ? { "If-None-Match": etag } : {},
signal,
});
if (res.status === 304) return { data: last, changed: false };
if (!res.ok) throw new Error(`HTTP ${res.status}`);
etag = res.headers.get("ETag");
last = await res.json();
return { data: last, changed: true };
}
When everyone polls the same thing: let a CDN answer
Some polled data is the same for every user: a match score, a public status page, the price of a concert ticket tier. Then you don't need your servers to answer each poll at all. Send Cache-Control: public, max-age=5 and a CDN (a network of caches near users) answers every poll for 5 seconds from its copy.
A million fans polling a score every 5 seconds is 200,000 requests per second at the CDN, but your origin sees about one request per 5 seconds from each CDN location: a few dozen requests per second in total. The cost is up to 5 seconds of extra delay, which is usually fine for a score.
Pushing the same score over WebSockets would mean a million open connections to run. Personal data (my orders, my inbox) can't be shared this way: mark it private so shared caches never store it.
Check your understanding · Exercise 6
Your poll sends If-None-Match: "v42" and the data has not changed. What is true? Select all that apply.
Spreading the load: jitter and server hints
So far we have looked at one client. Your server sees all of them at once, and the way their polls line up in time matters as much as how many there are. 2,000 requests per second spread evenly is easy. The same requests arriving in one burst at the top of every 10 seconds is a spike that needs ten times the capacity.
How clients fall into step
Clients start polling at random times, so their polls are usually spread out. Then something lines them up, and plain fixed intervals keep them lined up forever. Common causes:
- A deploy or outage. Every client's poll fails at once, every client backs off by the same 2, 4, 8 seconds, and they all come back in the same second. This is the thundering herd: the burst can knock the server over again just as it recovers.
- A shared event. A match kicks off, a sale opens at 9:00, or a push notification brings 50,000 people to the same page in the same minute.
- Clock-aligned code. Polling "on every full minute" (
setTimeoutuntil:00) makes every client in the world ask at the same instant.
Jitter: add randomness to every wait
Jitter is a small random change to each wait so that clients drift apart. Instead of waiting exactly 10 seconds, each client waits a random time between 8 and 12 seconds. The average interval, and so the average load, stays the same, but the polls spread across the whole window instead of stacking in one moment.
No jitter: each client waits exactly 10 s. Busiest second: 10,000 requests. Whole minute: 50,000 requests.
For backoff after errors, use more randomness. A common choice is "full jitter": pick a random wait between zero and the current backoff limit. If the limit is 16 seconds, one client waits 3, another 11, another 15. The AWS Architecture Blog showed that full jitter finishes the work with far fewer total calls than backoff without jitter, because clients stop colliding.
// polling: plus or minus 20% around the target
const withJitter = (ms: number) => ms * (0.8 + Math.random() * 0.4);
// backoff after errors: "full jitter", anywhere from 0 to the cap
const backoff = (attempt: number) =>
Math.random() * Math.min(120_000, 2_000 * 2 ** attempt);
// first poll after page load: spread the herd too
await sleep(Math.random() * 1_000, signal);
Server hints: let the server set the pace
The client guesses when to ask again, but the server often knows. It knows the restaurant just started a 20-minute dish, that the export job is 30% done, or that it is overloaded right now. A good polling API lets the server tell the client when to come back. There are three common ways.
- A field in the body.
{ "status": "cooking", "pollAfterMs": 30000 }. The simplest and most flexible: the server can change it per resource, per user, and per moment, and the client just obeys it, with jitter and within sane bounds. - A header. GitHub's REST API sends
X-Poll-Interval: 60on its events endpoints, telling clients how many seconds to wait, and raises it when its servers are busy. A header works the same on every endpoint and keeps the body unchanged. Retry-After. The standard HTTP header, sent with429 Too Many Requestsor503 Service Unavailable. It holds either a number of seconds (Retry-After: 30) or an HTTP date (Retry-After: Wed, 21 Oct 2026 07:28:00 GMT), so parse both: try the number first, otherwise subtractDate.now()fromDate.parse(value). When it is present, wait at least that long, whatever your own backoff says. Some APIs also send it with202 Accepted, as in the job example later in this lesson.
A standard example of server-led polling is the OAuth device flow (RFC 8628), used when you sign in to a TV app with your phone. The TV polls the token endpoint every interval seconds (5 by default). If it polls too fast, the server answers slow_down, and the TV must add 5 seconds to its interval for all later polls.
Server hints turn the interval into a dial the backend team can turn during an incident. If the database is struggling, raise pollAfterMs from 5 to 30 seconds and the load drops by more than 80% within about half a minute, with no app release. Clamp the hint on the client (for example between 1 and 300 seconds) so a server bug can't make clients poll every millisecond or never again.
Check your understanding · Exercise 7
After a 2-minute outage, 40,000 clients that were backing off without jitter all retry in the same second. What helps? Select all that apply.
Polling for job status
The most common and least controversial use of polling is waiting for a slow job: exporting a 50 MB CSV, transcoding an uploaded video, generating a PDF invoice, running a CI build, or confirming a payment with a bank. The work takes from seconds to minutes, far longer than one HTTP request should stay open.
Holding the request open until the job ends is fragile. Proxies and load balancers cut idle requests after 30 to 120 seconds, a phone switching from Wi-Fi to 4G drops the connection, and the user can't refresh the page without losing the result. The answer is the asynchronous request-reply pattern: start the job in one request, then poll a status resource until it is done.
The flow, step by step
- The client asks for the work:
POST /exportswith the filters. - The server puts the job on a queue and answers at once with
202 Accepted, a job id, and aLocation: /exports/e_91header pointing at the status resource. - The client polls
GET /exports/e_91. Each answer says the state (queued,running,succeeded,failed), often a progress number, and when to ask next. - When the state is
succeeded, the answer includes where to get the result, such as a short-lived download URL. When it isfailed, it includes an error the UI can show. Either way, polling stops.
202 hands back a status URL, and each poll is a quick read of the job record.POST /exports HTTP/1.1
Content-Type: application/json
{ "range": "2026-Q3", "format": "csv" }
HTTP/1.1 202 Accepted
Location: /exports/e_91
Retry-After: 2
{ "id": "e_91", "status": "queued" }
GET /exports/e_91 HTTP/1.1
HTTP/1.1 200 OK
{ "id": "e_91", "status": "running", "progress": 0.4, "pollAfterMs": 3000 }
Designing the job resource well
- Make the job a real resource with an id. The id survives a page refresh: store it in the URL (
/exports?job=e_91) or inlocalStorage, and the page can pick up polling where it left off. - Make starting the job safe to retry. If the
POSTtimes out, the client doesn't know whether the job started. Send an idempotency key (a unique id per click) so a retriedPOSTreturns the same job instead of starting a second export. - Give progress, not just a spinner. A
progressfield from 0 to 1 lets the UI show a real progress bar, which makes a 40-second wait feel much shorter. Announce milestones (25%, 50%, done) through a polite live region, not every poll. - Match the interval to the job. A payment check that usually finishes in 3 seconds can poll every second for the first 10 seconds. A video transcode that takes 10 minutes should poll every 10 to 30 seconds. The server knows the expected duration best, so let it send the wait.
- Give up eventually. After a time limit (say 5 minutes for a payment), stop and show a clear message with a way to check later. Don't poll forever.
You will meet this pattern in many cloud APIs. The AWS SDKs ship "waiters" that poll a resource (for example, until a server is running) with growing, randomized delays and a maximum wait. Azure's long-running operations return a status URL to poll. Payment providers such as Stripe send the final result to your server through a webhook (an HTTP request from their server to yours), while the page polls your own server for the status.
Think first
A user clicks "Export" for a report that takes about 2 minutes, then refreshes the page after 30 seconds. How should your design handle that?
The export is a server-side job with an id, so the refresh loses nothing on the server. Put the job id in the URL or localStorage when the job starts. On load, the page sees the id, polls GET /exports/e_91 right away, shows the current progress, and keeps polling with the server's pollAfterMs.
If the user clicks "Export" again by mistake, the idempotency key (or a check for a running export with the same filters) returns the existing job instead of starting a second one.
When polling is the right answer
Short polling has a reputation as the naive option you replace with WebSockets once you are serious. That is wrong more often than it is right. Every technique in this module pays for lower delay with more moving parts, and many features don't need the lower delay.
What polling gets for free
- Plain, stateless HTTP. Any server can answer any poll. Deploys, autoscaling, and serverless functions just work, because no server holds a connection to a client. A deploy of a WebSocket server, by contrast, drops every connected user, who all reconnect at once.
- Works everywhere. Corporate proxies, firewalls, and old load balancers that break long-lived connections pass polls without trouble.
- Caching. Polls are ordinary
GETrequests, soETag, the browser cache, and CDNs all apply. No streaming technique can serve a million viewers from a CDN this simply. - Easy to run. You debug it in the DevTools Network tab, test it with plain HTTP mocks, rate-limit it with existing tools, and monitor it with existing dashboards.
- Recovery is built in. Every poll asks for the current state, so a client that missed updates catches up on its next poll. Streams need resume logic (like SSE's
Last-Event-ID) to avoid gaps.
Decision Order tracking for a delivery app: how should the status page get updates?
What we pick: Short polling for the status page: a few changes per order, a delay of a few seconds goes unnoticed, and the backend stays simple. Move the driver's live map position to SSE if product wants smooth movement, since that changes every second or two.
When to move to something else
Switch away from polling when one of these is true, and say which one in your design:
- The delay must be under about a second. Chat messages, typing indicators, live cursors, multiplayer games, trading screens. Polling every 500 ms to get there is the worst of both worlds.
- Each user has their own fast-changing data. CDN caching can't help, and the empty-answer ratio is huge. SSE or WebSockets send only when something changes.
- The client also sends often. Collaborative editing or a game sends many small messages up as well as down: that is what WebSockets are for.
- Changes come faster than the interval. A value that changes 10 times a second, polled every 2 seconds, shows only 1 in 20 of the states. If each change matters (log lines, chat messages), polling for the latest state loses them unless the API returns everything since a cursor.
Mixing techniques in one product
Real products rarely pick one technique for everything. A chat app streams messages over a WebSocket but polls once a minute for the unread count on other workspaces. A CI dashboard streams the log of the build you are watching but polls the status badges of the 30 others in the list. A ride-hailing app might poll the trip status and stream the car's position.
Polling is also the standard fallback. When a WebSocket or SSE connection keeps failing (a strict corporate proxy, for example), many clients drop to polling the same data so the feature still works, just with more delay. If your data layer hides the transport behind one interface, such as a subscribe(orderId, onChange) function, you can start with polling and switch later without touching the UI.
Check your understanding · Exercise 8
Which features are a good fit for short polling? Select all that apply.
Using it in a system design interview
Polling comes up in almost every frontend system design interview that has data changing on the server: a news feed with "new posts", a notifications badge, an order or ride tracker, a file upload, a dashboard, a CI page. The interviewer wants to see that you pick the real-time technique on purpose, size its cost, and know how to make it cheap.
When to bring it up
Bring it up during the high-level design, when you list how each piece of data reaches the page. For each one, state how fresh it must be and how often it changes, then pick a technique. Saying "the feed polls for new posts every 30 seconds, the chat streams over a WebSocket" shows judgment that "we'll use WebSockets" for everything does not.
A strong answer in five steps
- Requirement. "An order changes state about six times in 40 minutes, and a delay of a few seconds is fine."
- Size it. "At peak, 20,000 open tracking pages polling every 10 seconds is 2,000 requests per second. Most return nothing new."
- Cut it. "Pause hidden tabs and poll on return, adapt the interval from 2 to 30 seconds, use ETag and 304, and add jitter."
- Let the server steer. "Each answer carries a
pollAfterMs, and the client respectsRetry-Afteron 429 or 503, so we can slow the fleet during an incident without a release." - Name the exit. "If product wants the driver's position to move smoothly, that part moves to SSE behind the same
subscribeinterface."
How deep to go by level
| Level | What a good answer shows |
|---|---|
| Junior | Knows polling, long polling, SSE, and WebSockets exist and what each does; picks polling for rare updates and says why; uses setTimeout, not setInterval. |
| Mid | Does the requests-per-second math, pauses hidden tabs, handles errors with backoff, uses ETag and 304, stops at a final state, shows "last updated" when stale. |
| Senior | Adaptive intervals, jitter, server hints and Retry-After, CDN caching for shared data, the job-status pattern with idempotent start, a clean switch path to SSE. |
| Staff | Treats the interval as an operational dial for incidents, plans the fleet's behavior after an outage, shares one poller across tabs, and mixes techniques per data type with a single client data layer. |
Traps to avoid
- Dismissing polling as naive. Jumping to WebSockets for an order status suggests you don't weigh cost. Say what you gain and what you pay.
- Forgetting the math. "We'll poll every second" with no load estimate invites the question "for how many users?".
- Polling for everything a stream should carry. Chat over 1-second polling is a red flag: the delay is still noticeable and the load is huge.
- No plan for failure. An interviewer will ask "what if the API is down?". Have backoff with jitter, stale-data messaging, and Retry-After ready.
Think first
The interviewer pushes back: "Polling is wasteful, why not WebSockets?" How do you answer for a notifications badge that changes a few times a day per user?
Agree that most polls return nothing, then show it is cheap: with hidden tabs paused, a 60-second interval, and ETag, a visible tab costs one tiny 304 a minute. 100,000 visible tabs is about 1,700 small requests per second that any stateless API handles.
A WebSocket would need 100,000 always-open connections, a pub/sub system to route each notification to the right socket, reconnect handling, and connection draining on every deploy, all to deliver a few events per user per day. If the product later adds chat, the socket becomes worth it, and the badge can ride on it then.
Summary
- Short polling asks the server "anything new?" on a timer. It is plain, stateless HTTP: any endpoint can be polled, and proxies, CDNs, and deploys just work.
- The interval sets both costs: the average delay is half the interval, and the load is open pages divided by the interval (20,000 pages every 10 s is 2,000 requests per second).
- Schedule each poll after the last answer with a setTimeout chain, with a time limit per request and a clean stop. setInterval overlaps requests on slow networks.
- Adapt the interval: start short, slow down while nothing changes, reset on a change or user activity, stop at a final state. Back off harder on errors.
- Pause in hidden tabs and poll at once on return; browser throttling of hidden timers is uneven and is not a plan. Stop while offline, and share one poller across tabs.
- Make empty answers cheap with ETag, If-None-Match, and 304, from a version the server can check cheaply. Let a CDN answer polls for data everyone shares.
- Add jitter to every wait so clients don't fall into step, and let the server set the pace with a next-poll field and Retry-After.
- Poll when updates are rare and a few seconds is fine, and for job status. Move to SSE or WebSockets when updates must arrive in under a second or flow both ways.
In the interview
Questions interviewers ask about this. Answer each out loud first, then compare.
Question 1
30,000 users keep your page open and it polls every 5 seconds. How many requests per second is that, and how would you cut it without making updates slower when they matter?
30,000 divided by 5 is 6,000 requests per second. First, pause on visibilitychange: many of those tabs are hidden, and pausing them removes a large share at once, with an immediate poll when the user comes back.
Then make the interval adaptive: slow down while nothing changes, reset on a change or user activity, and stop at a final state. Make the remaining polls cheap with ETag and 304 from a cheap version check, add jitter, and send a pollAfterMs so the server can slow clients during an incident. If the data is the same for everyone, let a CDN cache it for a few seconds. Only if updates must arrive within a second would I move to SSE.
Question 2
Why is a setTimeout chain better than setInterval for polling?
setInterval fires on the clock whether or not the last request finished. When responses are slower than the interval, requests overlap, pile up on an already slow server, and can finish out of order, so old data overwrites new data.
A setTimeout chain schedules the next poll only after the answer, so there is one request in flight, clients slow down when the server does, and the wait can change every turn for adaptive intervals, backoff, jitter, and server hints.
Question 3
How does ETag-based polling work, and what does it save?
The server sends ETag: "v42" with the data. The client sends If-None-Match: "v42" on the next poll. If nothing changed, the server answers 304 Not Modified with no body, and the client keeps its data.
It saves the body bytes, JSON parsing, and re-rendering, but not the round trip. It saves server CPU only if the server can check the version (a version column or a cached number) without building the full response. Across origins, it costs a CORS preflight and needs ETag in Access-Control-Expose-Headers.
Question 4
Design the frontend for a report export that takes 1 to 3 minutes.
POST /exports with an idempotency key returns 202 Accepted with a job id and a status URL. The page stores the id in the URL so a refresh resumes, then polls GET /exports/:id with the server's pollAfterMs, shows a real progress bar, and announces milestones in a polite live region.
On succeeded it shows a download link; on failed an error with retry; after a time limit it stops and says the file will appear in the exports list. Polling pauses in hidden tabs unless the user asked to be told when it is ready, in which case it slows to about once a minute.
Question 5
When would you choose short polling over WebSockets, and what would make you switch?
When updates are infrequent and a few seconds of delay is fine: order status, build badges, a notifications count, job progress. Polling keeps servers stateless, works through every proxy, caches on a CDN, and recovers from missed updates on the next poll.
I would switch when updates must arrive in under a second, when each user has their own fast-changing data so most polls are wasted, or when the client also sends often. I keep the transport behind one subscribe function so the switch doesn't touch the UI.
Practice it
These design problems lean on short polling.
References
- Page Visibility API MDN: visibilityState, the visibilitychange event, and how browsers treat hidden pages.
- Heavy throttling of chained JS timers beginning in Chrome 88 Chrome's rules for timers in hidden pages: checks once a second, and once a minute for long timer chains after 5 minutes hidden.
- Window: setTimeout() method MDN: minimum delays and timeouts in inactive tabs, including Firefox's limits.
- ETag MDN: entity tags and conditional requests with If-None-Match.
- Retry-After MDN: the standard server hint for when to try again, as seconds or an HTTP date.
- Exponential Backoff And Jitter AWS Architecture Blog: why randomized backoff beats plain exponential backoff.
- Asynchronous Request-Reply pattern Azure Architecture Center: 202 Accepted, a status endpoint, and polling for long jobs.
- RFC 8628: OAuth 2.0 Device Authorization Grant A standard polling protocol with a server-set interval and a slow_down response.
- REST API endpoints for events GitHub: an API built for polling, with ETag and the X-Poll-Interval header.
Read next
- Long pollingThe browser asks for updates and the server holds the request open until it has something to say: near-instant updates using nothing but ordinary HTTP, at the price of one parked request per user.
- Server-sent eventsOne HTTP response that never finishes: the server keeps writing small text events to it, and the browser's
EventSourceparses them, reconnects on its own, and resumes where it left off. The simplest push channel when data flows from server to client. - WebSocketsOne long-lived connection, opened by an HTTP handshake, on which browser and server can each send a message at any moment. The usual pick for chat, multiplayer, and collaboration, and the one where you own the protocol, reconnects, and scaling.
- Choosing a real-time techniqueHow to pick between short polling, long polling, server-sent events, WebSockets, and WebRTC for each feature, starting from who talks, how often, and how fresh the data must be, then checking scale, proxies, battery, and the fallback you will ship.
- Retries, timeouts, and backoffHow a client survives a flaky network and a struggling server: give every request a deadline, retry only the failures that can heal, space the retries out with exponential backoff and jitter, and stop retrying when the server or the network is clearly down.
- HTTP cachingThe browser's built-in cache, steered by headers on each response, which lets a repeat visit skip the network entirely or ask only "has this changed?": the cheapest speed-up on the web.