The problem with almost every online timer
There is an easy way to write a countdown and a correct way, and the easy way is everywhere.
The easy way is to call setInterval once a second and subtract one from a counter
each time it fires. It is four lines, it looks right on a developer's machine, and it is wrong
in two distinct ways that only show up when a real person uses it.
The first is drift. A browser never fires an interval exactly on the millisecond; each tick arrives a little late, and because the next tick is scheduled relative to the last one, that lateness is kept. It does not average out. It adds up, tick after tick, for the whole life of the timer.
The second is worse and less known: a lost tick is lost forever. When the tab's single thread is blocked — a heavy script, a garbage collection pause, a laptop resuming from sleep, or simply the browser throttling a tab you are not looking at — the missed ticks are not replayed. The counter picks up where it left off, permanently behind by however long the interruption was. This is why a timer you set, minimised and came back to is so often showing a number that is plainly a lie.
Neither problem is hard to avoid. Store the instant the timer is due to finish, then, on every frame, subtract the current clock reading from it. Nothing accumulates, so nothing drifts; a sample that never arrives costs nothing, because the next one re-derives the answer from scratch. That is what this page does, and the rest of this page is the measurement that shows the difference is real.
Two counters, side by side
Claims about timer accuracy are usually made with adjectives. We would rather show the runs.
The script that produced every number below lives in the repository as
scripts/drift.mjs, and it writes its results straight into the module the site
renders from, so no figure on this page has been transcribed by hand.
The method: two counters run side by side against the same monotonic clock. The
naive counter is setInterval(cb, 1000) with elapsed time taken as
ticks × 1000 — the four-line version described above. The timestamp counter is
sampled every 16 ms, roughly one animation frame, and computes elapsed time as
now − start — the method used here. For each whole second we record the moment
each counter first displayed that second. The ideal moment is exactly
k × 1000 ms, so the gap is that counter's lateness, in milliseconds. Lateness that
grows with the seconds is drift; lateness that stays flat is just sampling latency.
Four conditions were measured: sixty seconds idle, sixty seconds with the thread deliberately kept busy (40 ms of blocking work every 200 ms, which is an ordinary amount for a page with an animation on it), five minutes idle to check the drift rate is linear, and fifteen seconds containing one deliberate three-second freeze.
Each condition was run 5 times, not once, and the conditions were interleaved — one idle, one busy, one long, one stalled, then round two — so a noisy stretch of machine time is spread across all four conditions instead of landing on one. Every figure below is the median of those runs with the observed range beside it. That is not statistical decoration. An earlier version of this page published one run per condition to four significant figures, and repetition showed that the single idle run it quoted was the fastest of its kind while the single busy run was among the slowest, which made the gap between them look bigger than the machine can actually support.
Conditions 1 and 2: what a minute costs
Idle, the naive counter delivered 59 ticks in 60.07 seconds of real time. That tick count matters for reading the next number: the lateness below is the lateness at tick 59, not at tick 60, because a counter that is running behind has not yet delivered the ticks it still owes. At that tick the display was 347.7 ms late — a drift rate of 5.89 ms per tick, with the 5 runs spanning 5.67 to 6.52 ms.
With the thread kept busy the same counter was 670 ms late at tick 59, a rate of 11.36 ms per tick spanning 9.42 to 12.97 ms — about 1.9× worse on the medians. Pair the extremes of the two sets against each other, though, and that multiplier runs from 1.4× to 2.3×. The width of that spread is why the median is what gets quoted, and why no single pairing is. The timestamp counter displayed all 60 seconds in every run, with a worst lateness of 30.2 ms idle and 48.5 ms busy — the length of a frame or two, and not growing.
Condition 3: is the drift linear?
This matters, because the long-duration figures in the table below are projections and a projection is only honest if the thing being projected is linear. Over five minutes the naive counter drifted at 6.07 ms per tick (range 5.64 to 6.13) against 5.89 ms per tick over one minute (range 5.67 to 6.52) — a ratio of medians of 1.03, with the extremes of the two sets spanning 0.86 to 1.08. The two ranges overlap, which is the most that a few thousand ticks on one machine can honestly establish: nothing here suggests the rate changes with the length of the run.
The like-for-like check: the five-minute runs reached tick 298, so multiplying the one-minute rate by 298 ticks predicts 1756 ms of lateness at that point. The runs themselves produced 1808.1 ms, so the prediction was 52 ms under. The tick count is not a pedantic detail: predicting at 300 ticks and comparing it against a 298-tick measurement would inflate that error by 2 ticks' worth of drift, and an earlier version of this page did exactly that.
Condition 4: one frozen thread
This is the run that makes the case. The thread was blocked for three seconds, five seconds into a fifteen-second run — the software equivalent of glancing away.
| After one 3-second freeze | Naive tick counter | Timestamp method |
|---|---|---|
| Whole seconds displayed after 15.1 s of real time | 12 | 15 |
| Whole seconds lost and never recovered | 3 | 0 |
| Worst lateness reached during the freeze | 3052.7 ms | 3009 ms |
| Lateness still on the clock when the freeze ended | 3052.7 ms | none — the next sample re-read the clock |
Three seconds went in and three seconds came out — permanently. The naive counter showed 12 seconds where 15 had actually passed, and no amount of waiting gives them back, because there is no record anywhere that they were missed. The timestamp counter froze for exactly as long, then re-read the clock on its next sample and was immediately correct.
How far out a naive timer finishes, by length
Two of these rows were timed end to end; the rest apply the median per-tick rates to the tick count, which condition 3 showed is a fair thing to do. The point of the table is the shape rather than any single figure: the error is proportional to the length of the timer, so it is invisible on a one-minute egg and obvious on an exam paper. No run longer than five minutes was ever made, so every row below that is arithmetic on a rate, not a stopwatch reading.
| Timer length | Ticks | Late, idle page | Late, busy page | Directly measured |
|---|---|---|---|---|
| 1 minute | 60 | 354 ms | 681 ms | 347.7 ms at tick 59 |
| 5 minutes | 300 | 1.8 s | 3.4 s | 1808.1 ms at tick 298 |
| 10 minutes | 600 | 3.5 s | 6.8 s | — |
| 15 minutes | 900 | 5.3 s | 10 s | — |
| 20 minutes | 1,200 | 7.1 s | 14 s | — |
| 25 minutes | 1,500 | 8.8 s | 17 s | — |
| 30 minutes | 1,800 | 11 s | 20 s | — |
| 1 hour | 3,600 | 21 s | 41 s | — |
The bottom row is the one worth sitting with. An hour-long countdown written the easy way finishes about 21 s late on a page doing nothing, and about 41 s late on a page doing something. That is the difference between an hour timer you can put in front of an exam hall and one you cannot.
What we did not measure, and will not claim
These runs were made in Node on one Windows desktop, not in a browser on a fleet of machines, and it would be dishonest to present them as anything else. Node and browsers share the underlying behaviour that produces the effect — a repeating timer is re-armed after it fires, and missed firings are coalesced rather than replayed — but the exact per-tick overshoot depends on the engine, the operating system and how busy the machine is. Your numbers will differ. The shape is what transfers: accumulate and you drift, subtract from a fixed instant and you do not.
Browsers additionally throttle timers in tabs you are not looking at, and the policies are both documented by the browser vendors and subject to change between versions. We have not measured those policies ourselves, so this page does not put a number on them. It does not need to: a method that recomputes from the clock is correct regardless of how often it gets to run, which is precisely why it was chosen.
What the page does with all this
- One fixed end instant. Pressing start records
now + remaining. Every frame after that is a subtraction. Pausing captures the exact milliseconds left; resuming books a new end instant from them. - The alarm is booked, not played. Four tones are scheduled on the Web Audio clock when the timer starts, so they do not depend on a throttled script noticing zero. They are built from oscillators, so there is no audio file to fetch or fail. What this does not buy you is immunity from the browser suspending a silent audio context in a hidden tab — see the alarm question below, where that is set out in full.
- The tab title counts down. Deliberately short — the clock and the site name — so it stays readable when the browser has squeezed your tab to a sliver.
- Full screen with the controls out of the way. Press
F, or the button.Spacestarts and pauses,Rresets. - Every duration is a link. The seven length pages are permanent URLs, and
?t=takes anything from?t=45to?t=1h30m.
Which length do you want?
Each of these opens preset, carrying notes written for its own length — what people use it for, and how it fails.
- 1 minute timer — planks, held stretches, rests between sets, and why a freeze hurts short timers most.
- 5 minute timer — the default "just wait a moment", and the length we measured end to end.
- 10 minute timer — talk slots, tidies and the arithmetic of six slots to the hour.
- 15 minute timer — the quarter-hour grid that calendars and timetables are drawn on.
- 20 minute timer — naps and eye breaks, where the alarm matters more than the display.
- 30 minute timer — training blocks, oven slots and study sessions.
- 1 hour timer — exam sections and deep work, where drift finally becomes visible.
- Pomodoro timer — 25 on, 5 off, alternating on its own, with the cycle schedule worked out to the minute.