setInterval breaks in background tabs. Chrome throttles it to once per minute, Firefox enforces strict minimum intervals, Safari does something similar.
If your app relies on setInterval for status polling, countdown timers, or notification checks, it will fail when users switch tabs. The timer callbacks get delayed, batched or skipped. Your UI updates become sluggish or stop working.
This isn’t a bug, it’s deliberate. Browsers throttle background tabs to save battery and CPU. But it breaks timing-dependent features.
Why Web Workers Solve This
Web Workers run on a separate thread from the main UI. They aren’t subject to the same throttling rules as the main thread. Timers in Workers run consistently, even when the tab is inactive.
So use Workers for timing logic that needs to stay accurate.
When NOT to Use Web Workers
Workers aren’t free. They add complexity and memory overhead. Don’t use them for simple animations or UI updates that don’t need to run in background tabs. CSS animations and requestAnimationFrame are better for that.
Also skip Workers if your timing requirements are loose. If you’re polling every 5 minutes and a 30-second delay doesn’t matter, the main thread is fine. The throttling only hurts you when precision matters.
Workers make sense when:
- Background accuracy is critical (session timeouts, real-time status)
- You’re polling APIs at specific intervals
- Users expect consistency across tab states
- Timer drift would break functionality
If the tab being active matters more than background accuracy, stick with the main thread. Workers are for when you can’t afford to be throttled.
Basic Implementation
Here’s a countdown timer that needs to tick every second:
javascript
const workerCode = `
let remaining = 60;
const timer = setInterval(() => {
remaining--;
postMessage({ remaining });
if (remaining <= 0) {
clearInterval(timer);
postMessage({ done: true });
}
}, 1000);
`;
const blob = new Blob([workerCode], { type: 'application/javascript' });
const worker = new Worker(URL.createObjectURL(blob));
worker.onmessage = (e) => {
if (e.data.done) {
console.log('Timer finished');
worker.terminate();
} else {
document.getElementById('countdown').textContent = e.data.remaining;
}
};
The timer runs accurately in the Worker. The main thread receives updates via postMessage and updates the DOM. When the tab is inactive, the Worker keeps running—the countdown stays precise.
Real-World Example: Status Polling
More practical example, an app that polls an API for status updates:
javascript
const workerCode = `
setInterval(() => {
fetch('/api/system/status')
.then(res => res.json())
.then(data => postMessage({ status: data }))
.catch(err => postMessage({ error: err.message }));
}, 30000);
`;
const blob = new Blob([workerCode], { type: 'application/javascript' });
const worker = new Worker(URL.createObjectURL(blob));
worker.onmessage = (e) => {
if (e.data.error) {
console.error('Polling failed:', e.data.error);
return;
}
updateStatusIndicator(e.data.status);
};
// Clean up when done
window.addEventListener('beforeunload', () => {
worker.terminate();
This polls every 30 seconds regardless of tab state. Users get timely status updates even after switching tabs.
Implementation Notes
Keep Worker logic minimal. Heavy computation in Workers is fine, but for timing, keep it simple. The more work the Worker does, the more chance for delays.
Always terminate Workers. Call worker.terminate() when you’re done. Otherwise you leak memory and waste resources.
Use performance.now() for precision. If you need sub-second accuracy, combine setInterval with performance.now() to measure actual elapsed time and compensate for drift.
Workers can’t touch the DOM. All UI updates happen on the main thread via postMessage. Design your message format accordingly.
Test in background tabs. Open DevTools, set breakpoints in your Worker, then switch tabs. Verify your callbacks fire at the expected intervals.
setInterval breaks in background tabs—Chrome throttles it to once per minute. If your app relies on precise timing for polling, countdowns, or notifications, it will fail when users switch tabs. Here’s how Web Workers solve it.
Browser Support and Compatibility
Web Workers have solid support across modern browsers. They’ve been around since 2012. Chrome, Firefox, Safari, Edge all support them. Even mobile browsers handle Workers well, though they’re more aggressive with throttling on the main thread.
IE11 technically supports Workers but with quirks. If you’re still supporting IE11 (hopefully not), test thoroughly. The Worker API itself works, but some modern JavaScript features inside the Worker might not.
One catch: Workers don’t work with the `file://` protocol during development. You need a local server. Something as simple as `python -m http.server` or `npx serve` works fine.
Check https://caniuse.com for current compatibility, but unless you’re targeting very old browsers, you’re good to go.
Getting Started
Find your timing dependencies. Look through your codebase for setInterval or setTimeout. Which ones need to stay accurate when the tab is in the background? Common candidates: notification polling, countdowns, status indicators, session timers.
Browser throttling behavior is documented in MDN’s setTimeout reference if you want the specifics.
Move timing logic to a Worker
Extract the timer code into a Worker. Use postMessage for communication. If you need millisecond precision, combine setInterval with performance.now().
This Stack Overflow thread has several working examples if you want to see different approaches.
Test it
Switch tabs and verify timing stays consistent. Use DevTools to throttle CPU and network. Test on mobile. Browsers are more aggressive with throttling there. Let it run for a while to catch edge cases.
Common Issues
JavaScript timers aren’t real-time. Even Workers have some variance. Use performance.now() to measure actual elapsed time and compensate if you need millisecond accuracy.
No. Workers can’t access localStorage or sessionStorage. If you need to persist state, postMessage it to the main thread and handle storage there.
Use a Shared Worker instead of a regular Worker. Same concept, but the Worker instance is shared across contexts. More setup complexity, but useful for coordinating timing across tabs. Check MDN’s SharedWorker docs.
Call worker.terminate() from the main thread. This immediately kills the Worker and frees resources. Set it up in a cleanup handler or when your component unmounts.
Yes. Workers support modern JavaScript features. Just remember they can’t access DOM or window objects.
References
Now that you know this, how about deepening your perspective on this


