cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Timers in depth

Learn what JavaScript timers guarantee, how nested and hidden-page timers are delayed, and when message tasks are a better yield.

By the end, you can
  • 01
    Read timer guaranteesSeparate a minimum delay and ordering rule from a promise that a callback runs at an exact time.
  • 02
    Explain browser limitsModel the nesting clamp, background throttling, and Chrome intensive throttling conditions without relying on a stopwatch.
  • 03
    Choose 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.

Definition

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.

A zero-delay callback still runs laterPop out in the code editor (opens in a new tab)JavaScript
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.

Two same-delay timers keep creation orderPop out in the code editor (opens in a new tab)JavaScript
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.

Real-life analogyAn alarm clock

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.

A teaching model of the HTML clamp rulePop out in the code editor (opens in a new tab)JavaScript
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 the 4 ms clamp boundary
Step 0 of 3Ready
Your turn: follow the blue line

Step through a pure model of the nesting rule. The values are computed by the lesson's effectiveDelay function.

Running in
  1. script
Next: line 6
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
function effectiveDelay(nestingLevel, requestedMs) {  if (nestingLevel > 5 && requestedMs < 4) return 4;  return Math.max(0, requestedMs);} console.log(effectiveDelay(6, 0));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

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.

Playground: model a zero-delay timer chain
A small chain modelJavaScript
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(","));
Modeled delays8 ms total
level 00 ms

Requested zero delay in this model.

level 10 ms

Requested zero delay in this model.

level 20 ms

Requested zero delay in this model.

level 30 ms

Requested zero delay in this model.

level 40 ms

Requested zero delay in this model.

level 50 ms

Requested zero delay in this model.

level 64 ms

Clamped in this model.

level 74 ms

Clamped in this model.

Try it yourself

This teaching model has 8 scheduled callbacks. Levels above five use 4 ms, so the modeled total is 8 ms.

This calculation teaches the HTML nesting rule. It is not a stopwatch or a prediction of your device's elapsed time.

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.

Chrome timer groups described for Chrome 88
SituationPolicyWhat to remember
Visible or recently noisy pageMinimal throttlingThe normal nesting clamp still applies below 4 ms after enough chained timers.
Hidden page, ordinary timer groupAbout once per secondChrome checks this group once per second and can batch timers.
Hidden over five minutes, chain count at least five, silent 30 seconds, no WebRTCAbout once per minuteChrome'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.

Confirmed Chrome policy

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.

A shorter timer becomes eligible firstPop out in the code editor (opens in a new tab)JavaScript
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.

Current work finishes before the timerPop out in the code editor (opens in a new tab)JavaScript
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.

Step through a timer delayed by current work
Step 0 of 5Ready
Your turn: follow the blue line

Replay the deterministic ordering around a busy main thread. It records the lesson's busyTaskOrder model, not elapsed milliseconds.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
events.push("scheduled");events.push("start work");events.push("finish work");events.push("timer");console.log(events.join(" -> "));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

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.

Post a message taskPop out in the code editor (opens in a new tab)JavaScript
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.

Do not depend on a cross-source racePop out in the code editor (opens in a new tab)JavaScript
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.

Debounce a draft savePop out in the code editor (opens in a new tab)JavaScript
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.

Guarantee, browser policy, or not guaranteed?
  • 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.
Try it yourself
0 of 6 correct

Sort each claim. Read the explanation after every choice; timer code is easier when you name the kind of fact you rely on.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

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.
Timer ideas that are easy to confuse
PhraseWhat it meansWhat 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 delayA lower bound before eligibility, subject to browser scheduling.A deadline guaranteed by JavaScript.
The 4 ms ruleA nesting rule for timer initialization after level five.A universal fixed delay for all JavaScript work.
MessageChannelA 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

Exercise 1 · Warm-upPredict the clamp

Run the code mentally, then type the single number it prints.

Starter codePop out in the code editor (opens in a new tab)JavaScript
console.log(6 > 5 && 0 < 4 ? 4 : 0);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upPredict current work

    What prints first? Type the word, not the whole two-line output.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    setTimeout(() => console.log("timer"), 0);
    console.log("script");

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeFind the clamp boundary

      When a chain requests zero milliseconds, which nesting level first gets the clamp?

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeChoose a hidden-tab job

        Your app is hidden while a timer fires. What kind of job is safe to give that timer?

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeKeep only the latest save

          Which function cancels the older pending save in a debounce?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          let handle;
          function save() {
            clearTimeout(handle);
            handle = setTimeout(() => console.log("saved"), 0);
          }
          save();
          save();

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeApply this to a real app

            Your order-status page polls a server every second. What should you prefer when the server can announce a status change?

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              Answer each question by separating a specified guarantee from a browser policy and from an assumption the API never made.

              Timers in depth quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does setTimeout(fn, 0) mean?

                Choose an answer to see the explanation.

              2. Question 2 of 7What does this print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                setTimeout(() => console.log("timer"), 0);
                console.log("script");

                Choose an answer to see the explanation.

              3. 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.

              4. Question 4 of 7Which Chrome conditions are required for the intensive timer group described in Chrome 88?

                Choose an answer to see the explanation.

              5. Question 5 of 7What does a busy main thread prove about a timer callback?

                Choose an answer to see the explanation.

              6. Question 6 of 7Why might a scheduler use MessageChannel?

                Choose an answer to see the explanation.

              7. 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.

              CompleteFrontend Clear concepts. Working examples.