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 what a timer promisesTell the shortest wait and the order rules apart from a promise to run at an exact time.
- 02Explain the browser's limitsWork out the 4 ms rule for timer chains, and say when Chrome checks hidden-tab timers once a second or once a minute.
- 03Pick a way to run code laterExplain why schedulers may use
MessageChannel, and when a timer still says what you mean.
What a timer promises
A timer asks the browser to run a function later. The function you hand over is called a callback. A timer does not pause your code. It also does not promise an exact time.
Two waits happen before the callback runs:
- First, the browser waits for the delay you asked for.
- Then, it waits until it is free to run the callback.
The browser runs work in pieces called tasks. A task runs from start to end without a break. The part of the browser that picks the next task is called the event loop. It picks one task at a time.
A timer delay is the shortest wait, not an exact time. After the delay, the callback is ready. Ready means the browser may run it once the current work is done and the browser's own rules allow it.
This lesson follows Events, microtasks & ordering. That lesson showed the order of plain code, events, and microtasks. This lesson looks closely at timers: the limits browsers put on them, and the few rules you can count on.
Think of an alarm clock. An alarm set for zero minutes rings as soon as the clock can handle it. It does not cut into the sentence you are saying. A phone that saves battery may also check its alarms less often when nobody is using it.
Zero means later
The smallest timer example has two lines. Line 1 sets a timer and hands over a callback. Line 2 is plain code, so it runs straight away.
setTimeout(() => console.log("second"), 0);console.log("first");Line 1 sets a timer with a callback that prints second. Line 2 prints first right away. So the output is first, then second. A delay of 0 only means “no extra wait.” The callback still runs after the current script ends.
first right away, and the callback prints second after the script ends.Two timers with the same delay also follow a rule. The one you set first runs first. This rule is only about timers. It says nothing about other kinds of tasks.
setTimeout(() => console.log("first timer"), 0);setTimeout(() => console.log("second timer"), 0);Line 1 sets the first timer. Line 2 sets the second timer with the same delay. The output is first timer, then second timer. The script ends before either callback runs.
An alarm set for “now” does not cut into what you are doing. It rings at the next moment the clock can handle it.
- In real life: You set an alarm
- In JavaScript: You set a timer with a callback
- In real life: You finish your sentence
- In JavaScript: The current task keeps running
- In real life: The alarm rings when the clock checks
- In JavaScript: The event loop picks the timer's task
Timers inside timers: the 4 ms rule
A callback can set another timer. That timer's callback can set one more, and so on. This makes a chain of timers.
The browser counts how deep the chain is. This count is the nesting level. A timer set by plain code is level 0. A timer set inside its callback is level 1, and so on.
The HTML standard is the rule book that browsers follow. It has a rule for long chains. If the nesting level is more than 5 and you ask for less than 4 milliseconds (ms), the browser waits 4 ms instead. People call this the 4 ms clamp, because it clamps (raises) the delay to 4 ms.
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));This code is a model. It copies the rule, but it does not use real timers. Line by line:
- Line 1 names the function. It takes a nesting level and the delay you ask for.
- Line 2 checks both parts of the rule: a level more than 5, and a delay less than 4. If both are true, it returns 4.
- Line 3 returns the delay you asked for, but never less than 0.
- Line 6 prints
0, because level 5 is not more than 5. - Line 7 prints
4, because level 6 is more than 5.
Why a model and not a stopwatch? A stopwatch would also measure other things, like how busy the computer is and how each browser is built. The model shows only the rule.
Step through the model of the 4 ms rule. Each value comes from the lesson's own 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 alone is not a chain. The level goes up only when a timer's callback sets another timer. Both setTimeout and setInterval count. setInterval runs its callback again and again, and each repeat counts as one more level.
The playground below always asks for 0 ms. You change one thing: the nesting depth, which is how many timers are in the chain. For each level, it uses the same model, lists the wait, and adds the waits up. The total comes from the model. It is not how long your browser would really take.
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 msAsked for 0 ms. Waits 0 ms.
0 msAsked for 0 ms. Waits 0 ms.
0 msAsked for 0 ms. Waits 0 ms.
0 msAsked for 0 ms. Waits 0 ms.
0 msAsked for 0 ms. Waits 0 ms.
0 msAsked for 0 ms. Waits 0 ms.
4 msAsked for 0 ms. The rule raises it to 4 ms.
4 msAsked for 0 ms. The rule raises it to 4 ms.
The model has 8 timers in the chain, each asking for 0 ms. Levels more than 5 wait 4 ms, so the total from the model is 8 ms.
Up to level 5, each timer that asks for 0 ms waits 0 ms in the model. From level 6 on, each one waits 4 ms. Real work can make each callback even later. So don't build an exact clock out of a chain of timers.
Timers in a hidden tab
A tab is hidden when you can't see it. For example, another tab is in front of it, or its window is minimized. The browser decides when a page counts as hidden.
A browser does not need to wake a hidden page as often as a page you are reading. Waking it less often saves battery and computer power. Running timers less often like this is called throttling.
Chrome (from version 88) puts timers into groups. One group is for hidden pages where the lightest rules don't apply. Chrome checks the timers in this group once each second. Timers that are due around the same time may then run together at that check. Chrome uses this group when the page is hidden and any of these is true:
- The chain of timers is shorter than 5 timers.
- The page has been hidden for less than 5 minutes.
- The page uses WebRTC, the browser's tool for live video and voice calls.
Each row of this table is one Chrome group. Read across: when the group applies, how often Chrome checks its timers, and what to remember.
| When does it apply? | How often does Chrome check? | What should you remember? |
|---|---|---|
| The page is visible, or made sound in the last 30 seconds. | As asked. This is the lightest group. | The 4 ms rule still applies after enough timers in a chain. |
| The page is hidden, and the lightest group does not apply. | About once each second. | Timers due around the same time may run together. |
| Hidden over 5 minutes, a chain of 5 or more, silent for 30 seconds, and no WebRTC. | About once each minute. | Chrome calls this intensive throttling. |
The lesson for your code is simple. A timer in a hidden tab can run much later than you asked. So don't make it the only record of a countdown, a payment, or a server update. Instead, save the real clock time when something starts. When the page is visible again, work out what should have happened from that saved time.
Tabs hidden for a long time
Chrome 88 added a stricter group for hidden pages with long timer chains. Chrome calls it intensive throttling. It applies only when the lighter groups don't, and all of these are true:
- The page has been hidden for more than 5 minutes.
- The chain of timers is 5 or more timers long.
- The page has been silent (no sound) for at least 30 seconds.
- The page does not use WebRTC.
In this group, Chrome checks timers once each minute. Timers due around the same time may run together at that check. This is Chrome's own rule to save power. Other browsers do not have to do the same.
Think of the alarm clock again. A phone in battery-saver mode may check its alarms less often. After a long quiet time, it may check only about once a minute. That is good for the battery. But it means a timer that keeps asking “has anything changed?” is a poor way to notice a change.
When something changes, it is better to let the thing that changed tell you. Some examples:
- A server can send the page an update.
- An observer can tell you when an element changes. An observer is a browser tool that watches something and calls your function.
- A video or audio element sends an event when it plays or stops.
Timers are still fine when your feature really means “wait at least this long.”
Which timer runs first
Three timer rules are easy to mix up:
- A timer never runs before its delay.
- Two timers with the same delay run in the order you set them.
- If you set a short timer and then a longer one at the same time, the longer one does not run first.
None of these rules say a callback runs at an exact time.
setTimeout(() => console.log("short"), 0);setTimeout(() => console.log("long"), 20);console.log("scheduled");Line 1 sets a 0 ms timer. Line 2 sets a 20 ms timer. Line 3 prints scheduled right away. The order is always scheduled, then short, then long. This lesson makes no promise about how long each wait really is.
The HTML standard words this rule carefully. Before a timer runs, it waits for earlier timers of the same kind that asked for the same delay or less. That is why the order you set timers in matters. It does not make one fixed order for every kind of task.
When JavaScript is busy
A callback can be ready and still wait. The page's JavaScript runs on one main thread: one worker that does one thing at a time. A timer callback can't run in the middle of a task that is already running. These things can keep the main thread busy:
- a long loop
- working out the page layout, which is where every element goes
- another event handler, which is a function that runs when something happens, like a click
setTimeout(() => console.log("timer"), 0);console.log("start work");for (let score = 0; score < 3; score += 1) { // This work runs now. The timer has to wait until it ends.}console.log("finish work");Line 1 sets the timer. Line 2 prints start work. Lines 3 to 5 are a loop that runs right now. Line 6 prints finish work. Only after this task ends can the callback print timer. The lesson shows the order, never a measured time.
This explains many reports that a zero timeout is “slow.” Often the timer is not what made the page slow. It just had to wait behind work that was already running.
Step through the order of events when the script is busy. The list follows the lesson's busyTaskOrder model, not measured milliseconds.
script
events.push("scheduled");events.push("start work");events.push("finish work");events.push("timer");console.log(events.join(" -> "));setTimeout or MessageChannel
MessageChannel makes two connected ports, like the two ends of a tube. A message posted into one port comes out of the other. The other port's handler then runs in a later task, called a message task. So it runs later, like a timer. But it is not a timer, so the 4 ms rule does not apply to it.
const channel = new MessageChannel();channel.port1.onmessage = () => console.log("message task");channel.port2.postMessage("yield");console.log("scheduled");Line 1 makes the two ports. Line 2 sets the function that runs when port1 gets a message. Line 3 posts a message into port2. Line 4 prints scheduled right away. The output is scheduled, then message task.
Some schedulers use this trick. A scheduler is code that decides when other code runs. It can post a message to pause for a moment between chunks of work, so the browser can do other things. This does not mean a message task always runs before a timer task. Don't depend on which kind of task runs first unless a rule says so.
const channel = new MessageChannel();channel.port1.onmessage = () => console.log("message task");setTimeout(() => console.log("timer task"), 0);channel.port2.postMessage("yield");console.log("scheduled");Line 3 sets a timer. Line 4 posts a message. Line 5 still prints scheduled first. You can run this code and see what your browser does. But no rule fixes which of the two later tasks prints first.
Use timers for the right job
Timers are useful when waiting is part of what you want. For example:
- A save can wait until the person stops typing.
- A message can stay on screen for a short time.
- A failed request can wait a little before it tries again.
In each case, “later” is the goal. An exact time is not needed.
function saveDraft() { clearTimeout(saveDraft.timer); saveDraft.timer = setTimeout(() => console.log("saved"), 300);} saveDraft();saveDraft();This pattern is called debouncing: many quick calls turn into one. setTimeout gives back a timer ID, a number that names the timer. The function keeps that ID in saveDraft.timer.
- Line 2 cancels the save that is still waiting, with
clearTimeout. - Line 3 sets a new save timer for 300 ms.
- Lines 6 and 7 call the function twice. The second call cancels the first timer, so
savedprints only once.
Don't use a repeating timer just to ask “has this changed?” when something can tell you. Use these instead:
- A WebSocket or server-sent events for server updates. Both let the server send messages to the page.
IntersectionObserverto learn when an element scrolls into view.ResizeObserverto learn when an element changes size.- Media events to learn when a video or audio plays or stops.
- A timer callback runs before its delay is over.
- Two timers with the same delay run in the order you set them.
- A long script makes a ready callback wait.
- A hidden tab runs its timers together, less often.
- A timer in a hidden tab runs exactly once every minute.
- A
MessageChannelmessage skips the 4 ms rule.
Sort each sentence into a group. Read the explanation after each choice. Timer code is easier when you know which kind of fact you rely on.
Common wrong ideas about timers
“After N milliseconds” sounds stronger than it is. It means “not before N milliseconds.” It does not mean the browser stops everything at exactly N milliseconds. Busy work and the browser's own rules can make the callback later.
- “Zero means right now.” Zero means a later timer task, with no extra wait.
- “A delay is a deadline.” A delay is the shortest wait. The callback can run later.
- “A timer in a hidden tab works like a clock.” Hidden tabs check timers less often and may run them together.
- “MessageChannel always runs first.” Don't assume an order between different kinds of tasks unless a rule gives one.
Each row of this table takes one timer phrase. Read across: what it really means, then what it does not mean.
| What people say | What does it really mean? | What does it not mean? |
|---|---|---|
setTimeout(fn, 0) | Run fn in a later timer task, when the event loop can pick it. | Call fn right now. |
| A delay | The shortest wait. The browser may run the callback later. | A deadline that JavaScript keeps exactly. |
| The 4 ms rule | A rule for timer chains deeper than level 5. | A fixed 4 ms wait for all JavaScript work. |
MessageChannel | A way to run code later in a message task, in a browser page. | A replacement for every timer, or a task that always runs first. |
These limits are not bugs in timers. They keep the page quick to respond, stop hidden pages from waking for nothing, and save battery. Your code is safe when it still works if a callback runs a bit later.
Practice exercises
Work out the result in your head, then type the one number it prints.
console.log(6 > 5 && 0 < 4 ? 4 : 0);It prints 4. Level 6 is the first level in the model that is more than 5.
What prints first? Type the word, not both lines.
setTimeout(() => console.log("timer"), 0);
console.log("script");script prints first. The timer is set, but it can't cut into the script that set it.
A chain of timers asks for 0 ms each time. At which nesting level does the 4 ms rule first apply?
Level 6. It is the first level that gets 4 ms in this model when the chain asks for 0 ms.
Your app's tab is hidden when a timer is due. What kind of job is safe to give that timer?
Give it work that can wait, not a job with a strict deadline.
In a debounce, which function cancels the older save that is still waiting?
let handle;
function save() {
clearTimeout(handle);
handle = setTimeout(() => console.log("saved"), 0);
}
save();
save();clearTimeout(handle);
handle = setTimeout(() => console.log("saved"), 0);clearTimeout cancels the older save that is still waiting. The new timer is then the only save left.
Your order-status page asks the server for news every second. The server can also send a message when the status changes. What should you use instead of the timer?
Use an update the server sends, such as a WebSocket or server-sent events. It arrives when the change happens. A throttled timer can't be trusted as a clock.
Check your understanding
For each question, ask yourself: is this a rule timers always follow, a choice the browser makes, or something nobody promised?
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 standard's rule, when does a delay below 4 ms become 4 ms?
Choose an answer to see the explanation.
Question 4 of 7In Chrome 88, what must be true for Chrome to check timers only once a minute?
Choose an answer to see the explanation.
Question 5 of 7What does a busy main thread show 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 7A server can tell you when something changes. What is better than asking it again and again with a timer?
Choose an answer to see the explanation.
Key takeaways
- A timer delay is the shortest wait, not an exact time.
setTimeout(fn, 0)runsfnlater. It does not cut into the code that is running now.- Once the nesting level is more than 5, a request below 4 ms becomes 4 ms. This is the HTML standard's rule.
- Hidden tabs check timers less often and may run them together. Chrome may check only once a minute for tabs hidden a long time.
- Timer order rules are useful, but busy work can make every callback late.
MessageChannelruns code later without the 4 ms rule, but it does not always run before timers.
Remember the one-liner.
A timer says “not before this delay,” never “exactly at this moment.”
Coming next: Scheduling work. There you will meet tools made for scheduling, which say what you want more clearly than a timer can.
For everyday timer code, go back to the Timers lesson.