Timers & the Event Loop Phases
Reviewed & published by Brayan K
Node's timers (setTimeout, setInterval, setImmediate) and process.nextTick are scheduling tools that hand callbacks to specific phases of the event loop, which decides the exact order your asynchronous code runs in.
Part of the free Node.js course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
By the end of this lesson you'll be able to schedule and cancel timers, predict the precise order of nextTick, promise microtasks, timers, and setImmediate, and explain the event-loop phases that make Node fast and single-threaded.
What You'll Learn in This Lesson
- How to schedule and cancel work with setTimeout and clearTimeout
- How setInterval repeats and how to stop it with clearInterval
- Why setImmediate and setTimeout(fn, 0) are not the same thing
- How process.nextTick jumps ahead of every other callback
- The difference between microtasks (promises) and macrotasks (timers)
- The phases of the Node event loop and the order callbacks fire
1️⃣ The Timer Functions
Node gives you four scheduling globals that work without any require. setTimeout(fn, ms) runs a callback once after a delay and returns a handle; pass that handle to clearTimeout to cancel it before it fires. setInterval(fn, ms) repeats forever until you stop it with clearInterval.
The delay is a minimum, not a guarantee: Node will run your callback as soon as it can after that time, once the event loop reaches the timers phase. That's why the 30ms interval below ticks once before the 50ms timeout, then continues.
// Timers are global in Node — no require needed.
// setTimeout schedules a callback to run ONCE, after a delay (in ms).
const handle = setTimeout(() => console.log('never runs'), 1000);
// clearTimeout cancels a pending setTimeout before it fires.
clearTimeout(handle);
// This one is NOT cancelled, so it fires after ~50ms.
setTimeout(() => console.log('fires after ~50ms'), 50);
// setInterval repeats forever until you stop it with clearInterval.
let count = 0;
const id = setInterval(() => {
count++;
console.log(`tick ${count}`);
if (count === 3) clearInterval(id); // stop after 3 ticks
}, 30);2️⃣ The Order Everything Runs In
This is the part that trips up even experienced developers. After your synchronous code finishes, Node drains queues in a strict priority order: first the process.nextTick queue, then the promise microtask queue, and only then does the event loop advance to phases like timers (setTimeout) and check (setImmediate).
The example below schedules one of each, deliberately out of order, then numbers the output so you can see the true sequence. Run it in a .cjs (CommonJS) file to see this canonical ordering.
// Run this with: node order.cjs
console.log('1: synchronous start');
// These two jump the queue: nextTick and microtasks run as soon as the
// current operation finishes, before the loop moves on at all.
process.nextTick(() => console.log('3: process.nextTick'));
Promise.resolve().then(() => console.log('4: promise .then (microtask)'));
console.log('2: synchronous end');
// setTimeout(fn, 0) versus setImmediate is the ONE pair whose order is not
// fixed when you schedule them from the main module. It depends on how long
// the process took to start, so the same file genuinely prints them either
// way on different runs — measured here over eight runs, four each way.
// Any tutorial that promises you an order for those two from the top level
// is describing one run of one machine.
//
// Inside an I/O callback the answer IS defined, and this is why: the poll
// phase (where I/O callbacks run) is immediately followed by the check
// phase (where setImmediate runs), while timers only come round on the next
// turn of the loop. So schedule them there and the order is guaranteed:
require('node:fs').readFile(__filename, () => {
setTimeout(() => console.log('6: setTimeout(fn, 0) — timers phase, next turn'), 0);
setImmediate(() => console.log('5: setImmediate — check phase, this turn'));
});3️⃣ nextTick Always Beats Timers
The process.nextTick queue has the highest priority of all. Every nextTick callback registered during the current operation runs — to completion — before the event loop is allowed to move to the timers phase, even for a timer that was registered before the nextTick calls.
That power is a double-edged sword: if a nextTick callback schedules another nextTick, which schedules another, you can starve the loop and stop timers and I/O from ever running. Use it sparingly and intentionally.
// All process.nextTick callbacks drain BEFORE the event loop
// moves on to any timer — even ones registered earlier.
process.nextTick(() => console.log('A: nextTick before any timer'));
setTimeout(() => console.log('C: timer'), 0);
process.nextTick(() => console.log('B: second nextTick, still before timer'));Your turn. The program below works once you fill in the three blanks marked ___. Follow the 👉 hints, then run it and compare with the expected output — the order is the whole point.
// Run with: node turn.cjs
console.log('start');
// 🎯 YOUR TURN — fill each ___ with the right scheduler.
// 1) Schedule this to run in the NEXT tick, before promises:
___(() => console.log('nextTick')); // 👉 process.nextTick
// 2) Schedule this microtask with a resolved promise:
Promise.___().then(() => console.log('promise')); // 👉 resolve
// 3) Schedule this for the timers phase with zero delay:
___(() => console.log('timeout'), 0); // 👉 setTimeout
console.log('end');
// ✅ Expected output (order matters!):
// start
// end
// nextTick
// promise
// timeoutMini-Challenge: A Countdown Timer
No blanks this time — just a brief and an outline. Write it yourself, run it, and check your output against the example in the comments. This ties together setInterval and clearInterval in one short program.
// 🎯 MINI-CHALLENGE: a countdown timer
// Print 3, then 2, then 1 (one per ~100ms using setInterval),
// then print "Liftoff!" and STOP the interval with clearInterval.
//
// Outline:
// let n = 3;
// const id = setInterval(() => {
// if (n === 0) { /* print Liftoff!, clearInterval(id), return */ }
// // else print n and decrement
// }, 100);
//
// ✅ Example output:
// 3
// 2
// 1
// Liftoff!
// your code hereCommon Errors (and the fix)
- ❌ Your program never exits — A setInterval is still running. ✅ Store its id and call clearInterval(id) when you're done (or use unref() on the handle).
- ❌ Expected setTimeout(fn, 0) to run "now" — It can't; it's a macrotask. ✅ All synchronous code, nextTicks, and promise microtasks run first. Use process.nextTick if you truly need "before the next phase".
- ❌ Recursive process.nextTick froze my app — A nextTick that schedules another nextTick starves the loop. ✅ Use setImmediate to defer instead, so I/O and timers still get a turn.
- ❌ A wrong this inside the callback — Passing a method reference loses its binding. ✅ Use an arrow function: setTimeout(() => obj.run(), 100).
- ❌ Off-by-one delays — Timers fire after their delay, never before, and never with sub-millisecond precision. ✅ Don't rely on timers for exact timing; treat the delay as a minimum.
Pro Tips
- 💡 Defer with setImmediate, not nextTick, when you just want to "let the loop breathe" between chunks of work — it won't starve I/O.
- 💡 Call timer.unref() on a background timer so it won't keep your process alive on its own.
- 💡 Use import { setTimeout } from 'node:timers/promises' to await a delay cleanly instead of wrapping setTimeout in a promise by hand.
📋 Quick Reference — Scheduling & Order
| Scheduler | Runs |
|---|---|
| process.nextTick(fn) | before microtasks & every phase |
| Promise.then(fn) | microtask, after nextTick |
| setTimeout(fn, 0) | timers phase |
| setInterval(fn, ms) | timers phase, repeatedly |
| setImmediate(fn) | check phase |
🎉 Lesson Complete!
- ✅ setTimeout/setInterval schedule work; clearTimeout/clearInterval cancel it
- ✅ Order is: synchronous → process.nextTick → promise microtasks → timers → setImmediate
- ✅ setTimeout(fn, 0) is "very soon", not "now" — it's a macrotask
- ✅ process.nextTick beats timers but can starve the loop if abused
- ✅ The event loop cycles through timers, pending, poll, check, and close phases
Practice quiz
What does setTimeout(fn, ms) do?
- Runs fn repeatedly every ms
- Runs fn once after at least ms milliseconds
- Runs fn synchronously now
- Cancels a timer
Answer: Runs fn once after at least ms milliseconds. setTimeout schedules fn to run once after the delay (a minimum, not exact).
How do you cancel a pending setTimeout?
- clearTimeout(handle)
- stopTimeout(handle)
- setTimeout(null)
- delete handle
Answer: clearTimeout(handle). Pass the handle setTimeout returned to clearTimeout to cancel it before it fires.
After synchronous code, which queue drains FIRST?
- setTimeout timers
- setImmediate
- process.nextTick
- I/O callbacks
Answer: process.nextTick. The nextTick queue has the highest priority — it drains before microtasks and every phase.
In Node, which runs LAST among these in one turn?
- process.nextTick
- promise .then microtask
- synchronous code
- setImmediate
Answer: setImmediate. Order is sync → nextTick → promise microtasks → timers → setImmediate (check phase).
Why doesn't setTimeout(fn, 0) run immediately?
- It is a macrotask that waits for the timers phase
- It is cancelled automatically
- Zero means never
- It needs a require
Answer: It is a macrotask that waits for the timers phase. It runs after all sync code, nextTicks, and microtasks — it is a macrotask.
How do you stop a repeating setInterval?
- clearTimeout(id)
- clearInterval(id)
- return false
- setInterval(0)
Answer: clearInterval(id). Store the id setInterval returns and pass it to clearInterval.
Which is a microtask in Node?
- setImmediate
- setInterval
- A resolved promise's .then callback
- An fs read callback
Answer: A resolved promise's .then callback. Promise .then/.catch/.finally callbacks are microtasks; timers/immediate are macrotasks.
What risk comes from recursive process.nextTick calls?
- A SyntaxError
- The timer fires twice
- Faster I/O
- They can starve the event loop so timers and I/O never run
Answer: They can starve the event loop so timers and I/O never run. nextTick scheduling more nextTicks blocks the loop from advancing.
Which phase runs setImmediate callbacks?
- The check phase
- The timers phase
- The poll phase
- The close phase
Answer: The check phase. setImmediate callbacks run in the dedicated check phase, after poll.
Do the timer functions need a require in Node?
- Yes, require('timers')
- Only setInterval does
- No — they are global
- Only in ES modules
Answer: No — they are global. setTimeout, setInterval, setImmediate, and clearTimeout are global in Node.
Continue this course
- Previous: The process Object
- Next: Child Processes (exec & spawn) — Run external programs with exec, execFile, spawn, and fork
- Quick reference: Node.js cheat sheet
Frequently asked questions
What is the difference between process.nextTick and setImmediate?
They sound similar but fire at opposite ends of the loop. process.nextTick callbacks run immediately after the current operation finishes, before the event loop continues to any phase — so they jump ahead of timers, I/O, and even promise microtasks in some cases. setImmediate runs in the dedicated 'check' phase, after the current poll phase completes. Rule of thumb: process.nextTick is 'right now, before anything else'; setImmediate is 'on the next pass of the loop'. Overusing nextTick can starve the loop, so reach for setImmediate when you just want to defer work.
Why does setTimeout(fn, 0) not run instantly?
setTimeout(fn, 0) means 'run as soon as possible', not 'run synchronously'. The callback is placed in the timers phase of the event loop, which only runs after all synchronous code, all process.nextTick callbacks, and all pending promise microtasks have finished. Node also enforces a minimum delay (historically about 1ms), so a 0 is effectively a 'very soon, but not now' schedule.
What are microtasks versus macrotasks in Node?
Microtasks are the high-priority queue: resolved/rejected promise callbacks (.then/.catch/.finally) and queueMicrotask. They drain completely between each macrotask. Macrotasks are the larger units of work scheduled by timers (setTimeout/setInterval) and setImmediate, plus I/O callbacks. The pattern each turn is: run a macrotask, then empty the entire microtask queue, then move on. process.nextTick sits in its own even-higher-priority queue that drains before microtasks.
What are the phases of the Node event loop?
Each iteration of the loop moves through fixed phases in order: timers (setTimeout/setInterval callbacks whose time is up), pending callbacks (some deferred system callbacks), poll (retrieve new I/O events and run their callbacks), check (setImmediate callbacks), and close callbacks (like 'close' events on sockets). Between phases, Node drains the process.nextTick queue and then the promise microtask queue. Understanding this order explains why your callbacks fire when they do.