Accuracy cluster
See all guides tagged in the accuracy topic cluster.
Online timers are accurate to 10-50 ms per minute when the tab is active but drift in the background. Why JavaScript timers slip, and how to pick a steady one.
By Suraj Giri, productivity researcher at The Blog Timer. Updated 2026-05-27.
Browser-based timers are accurate within roughly 10 to 50 milliseconds per minute when the tab is active, but they can drift by seconds or even minutes when the tab is backgrounded, the device sleeps, or the system is under heavy load. The accuracy depends on whether the timer is built using setInterval, requestAnimationFrame, or a timestamp-based performance.now() loop. In our research at The Blog Timer, only timestamp-based timers maintain wall-clock accuracy across tab visibility changes.
JavaScript runs on a single-threaded event loop. When you call setInterval(callback, 1000), you are not asking the browser to invoke the callback exactly every 1,000 milliseconds — you are asking it to invoke the callback no sooner than 1,000 milliseconds from now, whenever the event loop is free. If a long-running script, a layout pass, or garbage collection blocks the loop, the next tick is delayed. Those delays accumulate.
This is why a naive setInterval-based timer drifts. If each tick is 5 ms late on average, after 12 minutes of “1-second” ticks, you have lost roughly 3.6 seconds. Over a 25-minute Pomodoro session, a busy tab can drift 5 to 10 seconds. That is unacceptable for productivity timers, where users expect wall-clock fidelity.
The fix is structural. Instead of counting ticks, you anchor to a timestamp:
performance.now() (or Date.now() as a fallback).elapsed = now - start.remaining = duration - elapsed.This pattern is self-correcting. If the event loop is blocked for 200 ms, the next frame still computes the correct remaining time. There is no accumulated drift, only momentary visual lag. Every accurate timer we have built — including the timers on this site — uses this pattern.
The performance.now() API returns a high-resolution timestamp in milliseconds, measured from the page’s navigation start. It has sub-millisecond resolution in modern browsers (often reduced to 1 ms or 100 µs for security reasons to prevent Spectre-class timing attacks). Crucially, it is monotonic — it never goes backward, even if the system clock changes — and it is not affected by the user adjusting their device clock or by daylight-saving transitions. It is the right primitive for measuring elapsed time.
Date.now(), by contrast, returns wall-clock time and can jump forward or backward if the system clock is corrected. Use it as a fallback, never as a primary clock for elapsed-time calculations.
Every modern browser throttles inactive tabs to save battery and CPU. Chrome, Firefox, Safari, and Edge each implement variants of the policy: when a tab is hidden, setInterval and setTimeout callbacks fire at most once per second, and after about five minutes of inactivity they may be throttled to once per minute or paused entirely. requestAnimationFrame is suspended completely in background tabs.
This means a timer that visually counts down using requestAnimationFrame will appear frozen if the user switches tabs — but if it is correctly anchored to performance.now(), the moment the tab becomes visible again, it recomputes elapsed time and snaps to the correct remaining duration. The visual is delayed, but the underlying time is not.
The Page Visibility API tells you whether the page is currently visible. Listening to the visibilitychange event lets a well-built timer:
Combining Page Visibility with a timestamp anchor and a server-time check (for very long timers) produces a timer that survives tab switching, lock-screen events, and brief sleep cycles.
requestAnimationFrame is the right primitive for the visual update loop. It runs synchronously with the display refresh (typically 60 Hz, sometimes 120 Hz on high-refresh devices), which means animations appear smooth. But it is not a clock — it does not run in background tabs, it can skip frames when the system is busy, and the time between frames is not guaranteed.
The right architecture: use requestAnimationFrame to schedule visual updates, and use performance.now() inside each frame to compute remaining time. This decouples display smoothness from timing accuracy.
iOS aggressively suspends background tabs and locks them when the screen turns off. A timer built only with JavaScript intervals will stop running when the user locks the phone. To survive screen lock, the timer must either (a) recompute elapsed time on resume using a stored start timestamp, or (b) use a service worker with the Notifications API to schedule an alarm. iOS also tightens timer resolution under low-power mode.
Android allows somewhat more background CPU than iOS, but recent Chrome versions implement intensive throttling: after a tab has been hidden for five minutes, timers fire at most once per minute. Service workers and Web Push notifications are the reliable path for scheduled alerts. The same timestamp-anchor pattern keeps visual timers accurate across resume.
Suppose a naive setInterval timer with a 1,000-ms interval is delayed by an average of δ milliseconds per tick. After N ticks, the accumulated drift is approximately N × δ. With δ = 8 ms, a 25-minute (1,500-tick) timer drifts about 12 seconds. With δ = 25 ms under load, the same timer drifts more than 35 seconds. Over a 60-minute session, the drift can exceed a full minute — enough to invalidate the timer entirely.
Timestamp-anchored timers reduce the drift to whatever resolution performance.now() provides (usually 1 ms or better) plus the rendering latency of the current frame (16.7 ms at 60 Hz). The drift is bounded, not cumulative.
Our internal testing protocol is straightforward but rigorous:
A correctly implemented timestamp-anchored timer passes all five tests with drift under one second.
| Timer Type | Accuracy (foreground) | Accuracy (background) | Survives sleep | Notifications |
|---|---|---|---|---|
| Browser (timestamp-anchored) | ±16 ms | Recomputed on resume | Yes (on resume) | Limited (tab must be open) |
| Browser (naive setInterval) | ±50 ms | Throttled, drifts | No | None |
| PWA with service worker | ±16 ms | Scheduled alarms | Yes | Push notifications |
| Native mobile app | ±1 ms | OS-level alarms | Yes | Local notifications |
| Physical kitchen timer | ±1-2 s/hour | N/A | Yes | Mechanical bell |
| Smartwatch timer | ±10 ms | OS-level | Yes | Haptic + visual |
A wind-up kitchen timer is dramatically less accurate than a digital clock — typical mechanical timers drift by 5-10% — but it has properties no software timer can match. It is glanceable from across the room, it does not steal attention by sitting on a screen full of notifications, and it cannot be silenced by a stray tap. For productivity work, the right timer is the one that does not betray the user. For some users that is a browser-based Pomodoro timer; for others it is a $4 plastic tomato.
Browser timers ultimately reduce to the system clock, which is itself synchronized using Network Time Protocol (NTP). NTP keeps consumer devices within about 10 ms of Coordinated Universal Time (UTC) under normal network conditions. For any timer that needs to be accurate across devices, sync to wall-clock UTC at start and compute everything from there.
When a user reports that a browser timer is “off by a minute,” the cause is almost always one of three things: the timer is naive setInterval-based and the user backgrounded the tab; the device entered a low-power state and the JavaScript runtime was suspended; or the page-visibility recompute step was missing from the implementation. None of these are bugs in JavaScript — they are bugs in how the timer was built.
A well-built browser timer has three layers:
performance.now() captured at start, used for every elapsed-time computation.requestAnimationFrame for smooth countdown rendering while the tab is visible.document.visibilitychange listener that recomputes elapsed time the instant the tab becomes visible again.Optionally, a fourth layer registers a service worker with a scheduled notification, so the alert fires even if the tab is closed or the device is asleep. Without that fourth layer, the timer is accurate but cannot notify in the background.
For productivity timers, drift is more than a cosmetic annoyance. A Pomodoro user who runs eight pomodoros per day with a 5% drift loses or gains roughly 60 seconds across the day. Compounded across a week of unsynchronized devices (browser timer at the desk, phone timer in the kitchen), users develop the (correct) impression that their tools cannot be trusted. Trust in a productivity tool is itself part of what makes the tool effective; a drifting timer corrodes that trust.
Before shipping our public timers, we ran the five-test protocol described above on every supported platform combination: Chrome on macOS, Safari on macOS, Chrome on Windows, Edge on Windows, Safari on iOS, Chrome on iOS, Chrome on Android, and Firefox on each desktop platform. The acceptable drift threshold is one second on a 25-minute timer for every combination. The implementation that passes uses the timestamp-anchor pattern with the page-visibility recompute hook; no naive setInterval timer passes.
Background tabs are throttled by every major browser to save energy. The fix is a timestamp-anchored timer that recomputes remaining time on visibility change rather than relying on continuous tick counting.
The API is universal, but resolution varies. Chrome and Edge return 100-microsecond resolution by default; Firefox returns 1-millisecond resolution; Safari intentionally adds jitter. For productivity timers, the differences are irrelevant — millisecond resolution is more than enough.
Not while it is asleep — JavaScript does not run when the device is suspended. But a correctly built timer recomputes the remaining time the moment the device wakes and the page becomes visible again. To receive an alert while the device is asleep, you need a service worker with notification permission, or a native app.
Anchor to performance.now() at start, recompute remaining time inside a requestAnimationFrame loop, listen to visibilitychange for recomputation on resume, and schedule a service-worker notification for the finish moment.
Modern smartwatches use the same OS-level timing primitives as their parent phones and are accurate to within tens of milliseconds. The bigger source of perceived error is the haptic delivery, not the underlying timer.
Run it next to time.is or another atomic-clock-synchronized reference for a 25-minute or longer block, and compare the finish moments. Repeat the test with the tab backgrounded for half the run.
One-second offsets usually come from rendering latency, not timing logic. The underlying time is correct, but the visual update happens on the next animation frame. For a productivity timer, this is invisible.
See all guides tagged in the accuracy topic cluster.
Yes. This timer uses your device's internal clock and tracks the end timestamp, not individual ticks. This means it stays accurate even if your browser tab goes to sleep or your device briefly lags.
Absolutely. This timer works on any device with a modern web browser—phones, tablets, laptops, and desktops. No app download required.