Timers in depth
Learn what JavaScript timers guarantee, how nested and hidden-page timers are delayed, and when message tasks are a better yield.
- 01Read timer guaranteesSeparate a minimum delay and ordering rule from a promise that a callback runs at an exact time.
- 02Explain browser limitsModel the nesting clamp, background throttling, and Chrome intensive throttling conditions without relying on a stopwatch.
- 03Choose a yielding toolExplain why schedulers may use MessageChannel and when a timer still communicates the right intent.
What a timer promises
A timer asks the browser to run a callback later. It does not pause the current script, reserve a slice of CPU, or promise an exact clock reading. The browser first waits for the requested delay, then waits until the event loop can choose the callback's task.
A timer delay is a lower bound before a callback becomes eligible. Eligibility means the browser may queue and run it when the current work and browser policy allow.
This follows Events, microtasks & ordering. That lesson separates synchronous work, event dispatch, and microtasks. This one narrows in on timer tasks, the browser limits around them, and the small guarantees that stay useful under load.
Think of an alarm clock. Setting an alarm for zero minutes means ring as soon as the clock can process it, not ring in the middle of the sentence you are saying. A phone trying to save battery may check alarms less often when it is not being used.
Zero means later
The smallest useful timer example has two lines. Read line 1 as “put this callback aside for a later timer task.” Read line 2 as ordinary synchronous JavaScript, which continues before the event loop gets another turn.
setTimeout(() => console.log("second"), 0);console.log("first");Line 1 schedules the callback that prints second. Line 2 immediately prints first. The real output is first, then second; a zero delay removed intentional waiting, not the boundary between current and future work.
Two equal delays have a useful ordering rule too. The first callback is created first, so it is the first same-delay timer to run when the browser reaches timer work. This is a timer rule, not a claim about every possible task source.
setTimeout(() => console.log("first timer"), 0);setTimeout(() => console.log("second timer"), 0);Line 1 creates the first callback. Line 2 creates the second callback with the same delay. The output is first timer, then second timer. The current script finishes before either callback runs.
Setting an alarm for now does not make it interrupt the moment you are already in. It rings at the next chance the clock can handle it.
- In real life: You set an alarm
- In JavaScript: You schedule a timer callback
- In real life: You finish your sentence
- In JavaScript: The current task keeps running
- In real life: The alarm rings when checked
- In JavaScript: The event loop selects a timer task
Where the analogy stops: A browser has many task sources and power policies, unlike one physical alarm clock.
Nesting levels and the 4 ms clamp
Nested timers are timers scheduled from timer callbacks. The HTML timer algorithm tracks a nesting level for that chain. When the nesting level is greater than 5 and a requested timeout is less than 4 milliseconds, the algorithm changes that timeout to 4 milliseconds.
function effectiveDelay(nestingLevel, requestedMs) { if (nestingLevel > 5 && requestedMs < 4) return 4; return Math.max(0, requestedMs);} console.log(effectiveDelay(5, 0));console.log(effectiveDelay(6, 0));Line 1 names the model function. Line 2 applies both parts of the rule: level greater than 5 and requested time below 4. Line 6 prints 0 because level 5 is not greater than 5. Line 7 prints 4 because level 6 crosses the boundary.
This is deliberately a model rather than a timing probe. It teaches the specified decision exactly, while a stopwatch would also include load, browser implementation choices, and the host machine.
Step through a pure model of the nesting rule. The values are computed by the lesson's effectiveDelay function.
script
function effectiveDelay(nestingLevel, requestedMs) { if (nestingLevel > 5 && requestedMs < 4) return 4; return Math.max(0, requestedMs);} console.log(effectiveDelay(6, 0));Model a chain of timers
One timer does not automatically make a long chain. The level rises when timer callbacks schedule more timer callbacks. That distinction matters because the rule is about nested invocations of timer initialization, including combinations of setTimeout and setInterval.
The playground fixes the requested delay at zero and gives you one input: nesting depth. It calls the same pure model for every level, lists the effective delays, and adds them. The displayed total is a teaching total, not a promise about elapsed browser time.
function nextTimer(level, requestedMs, delays) { const delay = level > 5 && requestedMs < 4 ? 4 : requestedMs; delays.push(delay); return delays;} const delays = [];nextTimer(5, 0, delays);nextTimer(6, 0, delays);console.log(delays.join(","));0 msRequested zero delay in this model.
0 msRequested zero delay in this model.
0 msRequested zero delay in this model.
0 msRequested zero delay in this model.
0 msRequested zero delay in this model.
0 msRequested zero delay in this model.
4 msClamped in this model.
4 msClamped in this model.
This teaching model has 8 scheduled callbacks. Levels above five use 4 ms, so the modeled total is 8 ms.
At depths through level 5, the model contributes zero for a zero request. At level 6 and above, each modeled nested callback contributes four milliseconds. Real work can make each callback later still, which is why an application should not build a precision loop from a timer chain.
Background-tab throttling
A browser does not need to wake a hidden page as often as a page the person is reading. Waking less often saves battery and reduces useless CPU work. Visibility is a browser decision, but an inactive tab or minimized window is generally hidden.
Chrome describes an ordinary throttling group for pages where minimal throttling does not apply. When the chain count is below five, the page has been hidden for less than five minutes, or WebRTC is in use, Chrome checks timers in that group once per second. Similar timeouts can batch at the same check.
| Situation | Policy | What to remember |
|---|---|---|
| Visible or recently noisy page | Minimal throttling | The normal nesting clamp still applies below 4 ms after enough chained timers. |
| Hidden page, ordinary timer group | About once per second | Chrome checks this group once per second and can batch timers. |
| Hidden over five minutes, chain count at least five, silent 30 seconds, no WebRTC | About once per minute | Chrome's intensive throttling checks the group once per minute. |
The useful design rule is modest: a hidden-tab timer can run much later than requested. Do not make it your source of truth for a countdown, a transaction, or a server update. Store real timestamps and calculate state when the page becomes active again.
Intensive throttling and budgets
Chrome 88 added a stricter group for chained hidden timers. Intensive throttling applies only when minimal and ordinary throttling conditions do not apply, and all of these are true: the page has been hidden for more than five minutes, the chain count is at least five, the page has been silent for at least 30 seconds, and WebRTC is not in use.
For that group, Chrome checks timers once per minute. It can batch similar timers on those minute-by-minute checks. This is a power budget policy, not a timer API guarantee shared by every browser.
The alarm-clock analogy helps again. A phone in battery-saver mode may check alarms less often in the background, and after a long quiet time it may check only about once a minute. That is kind to the battery, but it makes timer polling a poor way to detect a change.
When something has changed, prefer the thing that knows about the change to signal you. A server can push an update; an element can be observed; a media element can dispatch an event. Timers are still fine when your feature truly means “wait at least this long.”
Timer ordering guarantees
It is easy to mix up three rules. A timer never runs before its delay. Two same-delay timers created in order run in creation order. A shorter delay created at the same time is not overtaken by a longer delay. None of those rules say a callback runs at an exact timestamp.
setTimeout(() => console.log("short"), 0);setTimeout(() => console.log("long"), 20);console.log("scheduled");Line 1 creates a zero-delay timer. Line 2 creates a 20 millisecond timer. Line 3 logs scheduled synchronously. The stable output order is scheduled, then short, then long; the actual gap before each callback is not part of the lesson's claim.
The HTML timer algorithm also waits for earlier invocations with the same ordering identifier whose requested delay is no greater. That wording is why creation order remains meaningful for equal timer work without creating a universal order across unrelated task sources.
A busy main thread
A callback can be eligible and still wait. JavaScript cannot run a timer callback in the middle of a task that is already executing. A long loop, a large layout calculation, or another event handler can all keep the main thread busy.
setTimeout(() => console.log("timer"), 0);console.log("start work");for (let score = 0; score < 3; score += 1) { // Synchronous work finishes before the event loop can run the timer.}console.log("finish work");Line 1 schedules the timer. Line 2 prints start work. Lines 3 through 5 are synchronous work, and line 7 prints finish work. Only after that task completes can the callback print timer. The documented effect is the order, never a measured duration.
This explains many reports that a “zero timeout is slow.” The timeout is often not the work that made the page slow. It simply had to wait behind work already holding the main thread.
Replay the deterministic ordering around a busy main thread. It records the lesson's busyTaskOrder model, not elapsed milliseconds.
script
events.push("scheduled");events.push("start work");events.push("finish work");events.push("timer");console.log(events.join(" -> "));setTimeout versus MessageChannel
MessageChannel creates two connected ports. Posting a message to one port schedules a future message task for the other port's handler. It is asynchronous like a timer, but it does not initialize a timer and therefore does not use the timer nesting clamp.
const channel = new MessageChannel();channel.port1.onmessage = () => console.log("message task");channel.port2.postMessage("yield");console.log("scheduled");Line 1 creates the ports. Line 2 installs the callback. Line 3 posts a message, and line 4 logs scheduled in the current script. The output is scheduled, then message task.
Schedulers sometimes use this pattern to yield quickly between chunks of browser work. It is not a claim that a message task always beats a timer task. Different task sources should not be raced unless the relevant platform contract gives an order.
const channel = new MessageChannel();channel.port1.onmessage = () => console.log("message task");setTimeout(() => console.log("timer task"), 0);channel.port2.postMessage("yield");console.log("scheduled");Lines 2 and 3 schedule a timer task and a message task. Line 5 still prints scheduled first. The code is runnable so you can inspect one environment, but the lesson intentionally makes no fixed claim about which later task prints first.
Use timers for the right job
Timers are useful when waiting is part of a product decision. A save can wait until typing pauses. A message can disappear after a short minimum display time. A retry can wait before trying again. In each case, later is the desired behavior; exact precision is not.
function saveDraft() { clearTimeout(saveDraft.timer); saveDraft.timer = setTimeout(() => console.log("saved"), 300);} saveDraft();saveDraft();Line 2 cancels an earlier pending save. Line 3 schedules the latest one. Lines 6 and 7 call the function twice, so only the last callback prints saved. This is a small example of choosing a timer because the feature needs a quiet period.
Do not use a repeating timer just to ask whether something changed when an event exists. Use a WebSocket or server-sent event for a server update, IntersectionObserver for viewport entry, ResizeObserver for size changes, and events for media state.
- A timer callback runs before its requested delay.
- Two same-delay timers created in order.
- A long synchronous task delays a ready callback.
- A hidden tab wakes timers in batches.
- An eligible hidden timer runs exactly every minute.
- A MessageChannel post avoids the timer nesting clamp.
Sort each claim. Read the explanation after every choice; timer code is easier when you name the kind of fact you rely on.
Common timer misconceptions
The phrase “after N milliseconds” sounds stronger than it is. It means do not run before N milliseconds under the timer algorithm; it does not mean the browser must stop everything at exactly N milliseconds. Main-thread work and browser policy can make the observed callback later.
- “Zero means immediate.” Zero means a future timer task without an intentional delay.
- “A delay is a deadline.” It is a lower bound before eligibility.
- “A hidden interval is a clock.” Hidden-page throttling can batch and delay wake-ups.
- “MessageChannel wins every race.” Do not assume an order between task sources without a contract.
| Phrase | What it means | What it does not mean |
|---|---|---|
setTimeout(fn, 0) | Run fn in a later timer task when the event loop can choose it. | Call fn immediately. |
| A delay | A lower bound before eligibility, subject to browser scheduling. | A deadline guaranteed by JavaScript. |
| The 4 ms rule | A nesting rule for timer initialization after level five. | A universal fixed delay for all JavaScript work. |
| MessageChannel | A way to post a message task in a browser context. | A replacement for every timer or a task with universal priority. |
These limits are not defects in the API. They let browsers keep the page responsive, avoid needless background wake-ups, and choose device-friendly scheduling. Your code becomes robust when its correctness does not depend on a callback landing at one exact instant.
Practice exercises
Run the code mentally, then type the single number it prints.
console.log(6 > 5 && 0 < 4 ? 4 : 0);The expression prints 4. This is the first nesting level in the model that crosses the greater-than-five boundary.
What prints first? Type the word, not the whole two-line output.
setTimeout(() => console.log("timer"), 0);
console.log("script");script prints first. The timer was scheduled but cannot interrupt the script that scheduled it.
When a chain requests zero milliseconds, which nesting level first gets the clamp?
Level 6 is the first level that receives the 4 ms clamp in this teaching model for a zero request.
Your app is hidden while a timer fires. What kind of job is safe to give that timer?
Use it for best-effort work that can wait, not for a time-critical background deadline.
Which function cancels the older pending save in a debounce?
let handle;
function save() {
clearTimeout(handle);
handle = setTimeout(() => console.log("saved"), 0);
}
save();
save();clearTimeout(handle);
handle = setTimeout(save, 300);clearTimeout cancels the older pending callback. The new timer becomes the only pending save.
Your order-status page polls a server every second. What should you prefer when the server can announce a status change?
Prefer an event-driven update such as a WebSocket or server-sent event. It represents the change directly and avoids treating a throttled timer as a clock.
Check your understanding
Answer each question by separating a specified guarantee from a browser policy and from an assumption the API never made.
Question 1 of 7What does
setTimeout(fn, 0)mean?Choose an answer to see the explanation.
Question 2 of 7What does this print?
Read the code, then predictsetTimeout(() => console.log("timer"), 0); console.log("script");Choose an answer to see the explanation.
Question 3 of 7In the HTML timer rule, when does a requested delay below 4 ms become 4 ms?
Choose an answer to see the explanation.
Question 4 of 7Which Chrome conditions are required for the intensive timer group described in Chrome 88?
Choose an answer to see the explanation.
Question 5 of 7What does a busy main thread prove about a timer callback?
Choose an answer to see the explanation.
Question 6 of 7Why might a scheduler use MessageChannel?
Choose an answer to see the explanation.
Question 7 of 7What is a better response than timer polling when a server can tell you about a change?
Choose an answer to see the explanation.
Key takeaways
- A timer delay is a lower bound before eligibility, not an exact deadline.
setTimeout(fn, 0)schedules later work; it does not interrupt current JavaScript.- After nesting level five, requests below 4 ms are clamped to 4 ms by the HTML timer rule.
- Hidden pages can have their timers batched and delayed; Chrome can use minute checks for intensive throttling.
- Timer ordering rules are useful, but busy work can make every callback late.
- MessageChannel posts a task without the timer nesting clamp, but does not establish a universal race order.
Remember the one-liner.
A timer says “not before this delay,” never “exactly at this moment.”
Coming next: Scheduling work, where purpose-built priorities and yielding APIs make intent clearer than a timer.
For everyday timer patterns, revisit the published Timers lesson.