Understanding the Minimum Delay in setTimeout
JavaScript’s setTimeout looks simple on the surface: schedule a callback after a delay. Yet even when the delay is set to 0, the callback never executes instantly. Instead, browsers enforce a minimum delay, historically ranging from 0ms to 15ms, now standardized to 4ms under specific conditions.
To understand this behavior, we need to explore how timers evolved, how the event loop schedules callbacks, how browsers enforce nesting limits, and what alternatives exist when precise timing is required.
This article provides a deep, accurate, and practical explanation of the minimum delay mechanism and how it affects real-world code.
1. Why setTimeout Can Never Be Truly 0ms
Setting:
setTimeout(fn, 0);does not schedule immediate execution. Instead, it queues a task to be executed after:
- the current JavaScript stack finishes,
- microtasks (Promises, MutationObservers) are completed,
- and the browser enforces minimum delay rules.
Even under ideal conditions, this results in delays greater than zero.
2. Historical Evolution of the Minimum Delay
The minimum delay wasn’t always standardized. Through the early days of the web, different browsers imposed their own limits:
Timeline
- 1995 — Netscape Navigator: initial introduction
- 2003 — Internet Explorer: hard limit of 15.625ms due to reliance on Windows clock resolution
- 2009 — Firefox: adopts a 10ms minimum
- 2010 — HTML5 spec: standardizes a 4ms minimum when timer nesting depth ≥ 5
- Current browsers: follow the HTML standard, applying a 4ms clamp for deep nesting
HTML Specification Rule
If the timer nesting level reaches 5 or more:
minimum timeout = max(requested timeout, 4ms)Additionally:
- Negative delays are treated as 0ms
- Very small delays (<4ms) are allowed only for shallow nesting levels
This prevents pathological performance scenarios caused by tight looping timers.
3. How Browsers Enforce the Delay
3.1 Event Loop Mechanics
setTimeout is part of the task queue system:
- JavaScript executes the current stack.
- Microtasks run (e.g., promises).
- Tasks (macrotasks), including timers, are processed in FIFO order.
- When a timer “expires,” it becomes eligible to enter the queue — but entry is still subject to minimum delay rules.
3.2 Chromium Source Code
Relevant excerpt:
static const int kMaxTimerNestingLevel = 5;
static const double kMinimumInterval = 0.004; // 4msMeaning:
- If the nesting depth is ≥ 5, clamp delay to ≥ 4ms
- Depth increments when a timer schedules a new timer from inside itself
4. Measuring Actual Delays in Practice
The following demo prints the real time each callback takes to run:
function nestedTimer(depth = 0) {
const start = performance.now();
setTimeout(() => {
const actual = performance.now() - start;
console.log(`Depth ${depth}: delay ${actual.toFixed(2)}ms`);
if (depth < 10) nestedTimer(depth + 1);
}, 0);
}
nestedTimer();Typical output in modern browsers:
Depth 0 → ~0.3ms
Depth 1 → ~0.5ms
Depth 4 → ~1ms
Depth 5 → ~4.2ms ← clamp begins
Depth 9 → ~4.1ms5. Performance Implications
5.1 CPU Load and Battery Drain
Deeply nested timers:
- consume CPU cycles,
- prevent efficient browser scheduling,
- increase battery use (especially on laptops and mobile devices),
- cause high refresh jitter in animation loops.
5.2 Unexpected UI Latency
If you rely on setTimeout(fn, 0) to break up heavy work, expect inconsistent gaps between chunks.
6. Better Alternatives for Precise Timing
6.1 requestAnimationFrame (RAF)
Ideal for animations. Runs at the display’s refresh rate (typically 60Hz):
requestAnimationFrame(() => {
// smooth animation logic
});6.2 Web Workers
Timers in workers are not throttled when the tab is backgrounded.
Main script:
const worker = new Worker("worker.js");
worker.onmessage = () => console.log("tick");worker.js:
setInterval(() => postMessage("tick"), 1);6.3 performance.now() for Timing Calculations
Use this API for precise measurements, not for scheduling.
7. Practical Guidance
- Avoid depending on setTimeout precision for animation loops.
- Use timer scheduling to split heavy CPU tasks, but be aware of delays.
- Expect the 4ms clamp during nested timers.
- Use RAF for UI updates.
- Use workers for high-precision recurring computations.
- Avoid micro-optimizing zero-delay timers; the event loop ensures they’re non-zero.
Conclusion
The 4ms minimum delay in setTimeout is not a bug — it’s a deliberate and standardized design that balances compatibility, performance, battery life, and protection against runaway timer recursion.
Understanding how the browser schedules timers helps you write code that behaves predictably across platforms and performantly across devices. When precise timing matters, reach for APIs that were designed for that purpose, not for workarounds based on setTimeout(0).