A countdown decrements a seconds counter inside setInterval(tick, 1000) and renders it. On a busy page it is noticeably behind real time after an hour. Why does that happen, and how would you make it accurate?
answer
- ticks are late, never early
- the error has a sign
- how often you look, not what time it is
- derive the target from an origin
- sleep loses ticks entirely
basics
~20 sCounting ticks measures how many times the timer fired, not how much time passed. Each tick can only be late, never early, and the lateness accumulates. Compute remaining time from a timestamp captured at the start rather than from a tick count.
solid answer
~50 sA timer delay is a minimum, not an appointment: every tick lands at or after 1000 ms, never before, and with `setInterval` that lateness carries into the next scheduling decision, so the error accumulates monotonically across 3600 ticks. Whatever competes for the main thread — layout, a long task, a garbage-collection pause — adds to it, and if the device sleeps or the page is suspended, ticks are simply lost. The fix is to stop treating ticks as units of time and read a clock instead: capture a start timestamp or a deadline once, and on every tick compute the remaining time from `Date.now()` (or elapsed time from `performance.now()`). The tick rate then only controls how often you refresh the display, and being late costs you nothing. If you also need the tick to land near the second boundary, self-reschedule with `setTimeout` and subtract the measured lateness from the next delay.
code
javascript · 7 linesconst deadline = Date.now() + 60_000;
const id = setInterval(() => {
const remaining = Math.max(0, deadline - Date.now());
console.log(Math.ceil(remaining / 1000));
if (remaining === 0) clearInterval(id);
}, 250);go deeper
Recall that a timer delay is a minimum rather than a promise, and that a countdown should compute the remaining time from a stored deadline instead of counting how many times the callback ran.
Explain why the error only ever accumulates in one direction, and write the corrected version that derives the next delay from an absolute expected time rather than from the previous tick.
Bring the production cases: long tasks and GC pauses inflating lateness, suspension and sleep dropping ticks outright, and choosing between a monotonic clock and wall-clock time based on what the ground truth actually is.
Frame it as a design rule others can follow — timers schedule refreshes, clocks define state — and decide where authoritative time lives when a client countdown must agree with a server-side deadline.
## Why the counter drifts The bug is a category error: the code uses "number of timer callbacks that have run" as a proxy for "seconds elapsed". Those two quantities are only equal if every callback runs exactly 1000 ms after the previous one, and a timer never promises that. A timer delay is a **lower bound**. When the delay expires the callback becomes *eligible* to run; it still has to wait for the currently running task to finish and for anything queued ahead of it. So every tick is late by some amount `L >= 0`. The error has a sign — ticks can never be early — so it never cancels out. It integrates. Over 3600 ticks, an average lateness of two milliseconds is seven seconds of visible error; on a page doing real work, average lateness of tens of milliseconds is ordinary, and the countdown is minutes wrong. With `setInterval` specifically, the next occurrence is armed relative to the current (already late) dispatch, so today's lateness becomes part of tomorrow's baseline rather than being corrected away. ## And then there are the gaps Accumulating milliseconds is the mild case. The severe case is time during which the timer does not fire at all: the machine sleeps, the process is suspended, the main thread is blocked for seconds by a long synchronous task. When execution resumes, the tick count has not advanced but real time has moved on by minutes. No amount of per-tick correction recovers that, because the information was never in the tick stream to begin with — it is only in the clock. ## The fix: read the clock, do not count the chimes Decide what the timer is *for*. In a countdown, the ground truth is a deadline; the timer's only job is to decide how often you re-render. ```js const deadline = Date.now() + 60_000; const id = setInterval(() => { const remaining = Math.max(0, deadline - Date.now()); render(Math.ceil(remaining / 1000)); if (remaining === 0) clearInterval(id); }, 250); ``` This is correct under every failure mode above. A late tick renders the correct smaller number. A five-second stall renders a value five seconds lower when it resumes. Waking from sleep shows the true remaining time immediately. You can even change the tick rate for smoother updates without touching correctness — which is the tell that the design is right. ## Drift-corrected scheduling, when the tick itself must be aligned Rendering from the clock fixes *what you show*. Sometimes you also need *when you fire* to stay near the intended boundary — the second digit should change close to the real second, or a sampler should keep an even average rate. That calls for a self-rescheduling `setTimeout` whose delay is computed each cycle: ```js const interval = 1000; const start = performance.now(); let count = 0; function tick() { count += 1; const expected = start + count * interval; const late = performance.now() - expected; render(count); if (count < 3600) setTimeout(tick, Math.max(0, interval - late)); } setTimeout(tick, interval); ``` The key line is `expected = start + count * interval`: the target is derived from an absolute origin, not from the previous tick, so a late tick shortens only the *next* delay and the schedule pulls itself back rather than sliding. `Math.max(0, ...)` handles the case where a tick was so late that the next target is already past. ## Date.now versus performance.now Use `Date.now()` when the ground truth is wall-clock — a deadline, a timestamp shown to a human, a value compared against a server time. Use `performance.now()` when you are measuring *elapsed* time: it is monotonic and expressed as a floating-point millisecond offset from a page or process origin, so a user changing the system clock or an NTP correction cannot make it jump or run backwards. `Date.now()` can do exactly that, and a clock adjustment mid-countdown will visibly warp a naive elapsed-time calculation. Note that `performance.now()` resolution is deliberately coarsened by browsers for security reasons, so treat it as sub-millisecond-ish rather than exact. ## What an interviewer is listening for The weak answer is "timers are inaccurate, so use a shorter interval" — which changes nothing, because shorter ticks accumulate error faster. The strong answer names the sign of the error and its accumulation, distinguishes "how often I update" from "what time it is", picks the clock that matches the ground truth, and mentions the suspend/sleep case that no per-tick correction can fix.
- Would shortening the interval to 100 ms and counting tenths of a second improve the accuracy?No — it makes it worse. The error is per-tick lateness, so ten times more ticks accumulate error roughly ten times faster, and you have also multiplied the work the page does. Tick frequency should be chosen purely for how smoothly you want the display to update; correctness has to come from reading a clock.
- When would you use performance.now() instead of Date.now() here?When the quantity you need is elapsed time rather than a wall-clock moment. `performance.now()` is monotonic, so a user changing the system clock or an NTP correction cannot make your elapsed measurement jump or go backwards. For a countdown to a real-world deadline you want `Date.now()`, because the deadline itself is defined on the wall clock.
- The device sleeps for ten minutes mid-countdown. What does each version do on wake?The tick-counting version has not advanced its counter at all and resumes showing a time ten minutes too high. The clock-reading version renders the true remaining value on its very next tick, and if the deadline has passed it clamps to zero and clears itself. That gap is unrecoverable from ticks alone — only a clock has the information.
Counting timer ticks to tell the time is like counting chimes to know the hour: miss one or hear it late and the count is permanently wrong, whereas glancing at the clock is right every time no matter how many chimes you missed.
saying these in an interview costs you the question
- Suggests a shorter interval to reduce accumulated drift
- Assumes ticks can be early as well as late, so errors cancel
- Treats setInterval tick count as elapsed time
- Ignores that sleep or suspension drops ticks entirely
- Thinks correcting the delay once at startup is enough