Why Your Pomodoro App Isn't Accurate (And How to Fix It)
6 min read
You set a 25-minute Pomodoro timer. You switch to your work tab. Twenty-five minutes later, you glance at the clock and realize the timer still shows 3 minutes remaining — even though 28 minutes have actually passed.
This isn't a bug in one specific app. It's a fundamental problem with how most browser-based timers work, and it affects nearly every Pomodoro tool built with setInterval or setTimeout.
The setInterval Problem
The classic way to build a countdown timer in JavaScript:
let remaining = 25 * 60; // 1500 seconds
const interval = setInterval(() => {
remaining -= 1;
updateDisplay(remaining);
if (remaining <= 0) {
clearInterval(interval);
notifyUser();
}
}, 1000);
This looks correct. Decrement every second, display the result. What could go wrong?
Problem 1: Timer Throttling in Background Tabs
Modern browsers aggressively throttle setInterval and setTimeout in background tabs to save battery and CPU. Chrome, for example, limits background tab timers to fire at most once per second — but that's the best case. In practice:
- Chrome: Background tabs throttle timers to once per second, but can delay further to once per minute for tabs not playing audio.
- Firefox: Similar throttling behavior with 1-second minimum intervals.
- Safari: Even more aggressive — background timers can be paused entirely on macOS and iOS.
So your "every 1000ms" interval might actually fire every 5, 30, or even 60 seconds when the tab is in the background. Each missed tick is a second that doesn't get subtracted. After 25 minutes in the background, your timer could be off by several minutes.
Problem 2: Event Loop Delays
Even in the foreground, setInterval(fn, 1000) doesn't guarantee execution every 1000ms. JavaScript is single-threaded. If the main thread is busy (rendering, processing, garbage collecting), the callback gets queued and delayed.
These delays are usually small (5–50ms per tick), but they accumulate. Over a 25-minute session, this drift can add up to 10–30 seconds of inaccuracy.
Problem 3: System Sleep and Suspend
When your laptop goes to sleep, all JavaScript execution stops. setInterval doesn't "catch up" when the system wakes — it just resumes from where it left off, completely unaware that 20 minutes of wall-clock time passed.
The Fix: Timestamp-Based Tracking
The solution is simple in concept: don't count ticks. Track wall-clock time.
const endTime = Date.now() + 25 * 60 * 1000;
function updateTimer() {
const remaining = Math.max(0, endTime - Date.now());
updateDisplay(Math.ceil(remaining / 1000));
if (remaining > 0) {
requestAnimationFrame(updateTimer);
} else {
notifyUser();
}
}
requestAnimationFrame(updateTimer);
Instead of decrementing a counter, we calculate the remaining time from the difference between now and the target end time. This approach is immune to:
- Throttling: It doesn't matter how often the update function runs. When it does run, it shows the correct remaining time.
- Event loop delays: Same principle — the display catches up on every frame.
- System sleep: When the laptop wakes,
Date.now()immediately reflects the correct time. If the timer should have ended during sleep, it ends immediately.
Persisting Across Browser Restarts
Timestamp-based tracking has another advantage: you can persist the target end time and resume it after a page refresh or even a browser restart.
// On timer start
localStorage.setItem("timer_end", String(endTime));
// On page load
const savedEnd = localStorage.getItem("timer_end");
if (savedEnd && Number(savedEnd) > Date.now()) {
resumeTimer(Number(savedEnd));
}
This is how HushWork's timer works. Start a 25-minute session, close the tab, reopen it 10 minutes later — the timer shows 15 minutes remaining, exactly as it should. Try that with a setInterval-based timer.
Display Updates: requestAnimationFrame vs setInterval
For the display update loop, requestAnimationFrame is better than setInterval for two reasons:
- It syncs with the display refresh rate. No wasted updates between screen redraws.
- It pauses when the tab is hidden. This saves CPU — but because we're calculating from timestamps, the display is instantly correct when the tab becomes visible again.
For the notification trigger (playing a sound or showing an alert when the timer ends), we still need a fallback for background tabs. A setInterval at a low frequency (every 1000ms) is fine here because we're not using it for time tracking — just for checking whether Date.now() >= endTime.
The Page Title Trick
One pattern that makes timestamp-based timers more useful: update the page title with the remaining time.
document.title = `${minutes}:${seconds} — Focus`;
This makes the countdown visible in the browser tab bar even when you're working in another tab. You get the psychological benefit of seeing your timer without switching tabs and breaking flow.
HushWork shows the remaining time, the current mode (Focus or Break), and your session intention in the page title. Glance at your tabs to see "12:34 Focus — Write the API docs" without leaving your work.
Why This Matters for Focus
A timer that drifts by 3 minutes isn't just an engineering nuisance — it undermines the entire Pomodoro method. The technique works because of the contract: 25 minutes of focus, then a break. If the timer is unreliable, you lose trust in it. And once you lose trust, you start checking the clock yourself, which breaks the focus state the timer was supposed to protect.
Accurate timing also matters for statistics. If you're tracking daily focus minutes, inaccurate timers compound the error. Over a week of 6-8 Pomodoro sessions per day, you could be off by 20–30 minutes.
Testing Your Current Timer
Here's a quick test: open your Pomodoro app, start a 5-minute timer, switch to another tab, and set a phone alarm for 5 minutes. When your phone alarm goes off, switch back to the Pomodoro tab. If it shows anything other than 0:00, your timer has the setInterval problem.
Your Takeaway
If you're building a timer for the web, use timestamps. If you're using a timer app, test it in a background tab. And if you want a timer that just works — background tabs, laptop sleep, page refreshes — HushWork handles all of it with no sign-up required.
Related: The Pomodoro Technique: A Complete Guide · Flow State: How to Enter and Stay There · The 90-Minute Focus Cycle
Ready to try focused work?
Open HushWork →