Asyncadvanced
Node.js Event Loop Phases
How Node.js's libuv-based event loop differs from the browser's, with distinct phases for timers, I/O callbacks, and microtasks.
Node's event loop is libuv's, and it has named phases rather than one macrotask queue. The differences matter for ordering.
Node's event loop is like hospital ward rounds visiting timers, pending cases, the ER (poll), follow-ups (check), and discharge (close) in strict rotation — but urgent pages (nextTick, then microtasks) get answered immediately between every single patient visit.
Key Concepts
1
The phases, in order, repeating.
timers setTimeout, setInterval callbacks
pending some system callbacks
poll retrieve new I/O; execute I/O callbacks
check setImmediate callbacks
close 'close' event callbacks2
Microtasks drain between every phase, and process.nextTick has its own queue that runs before
promises.
process.nextTick
3
process.nextTick highest priority, before promises
Promise microtask queue
everything else by phase4
process.nextTick(() => console.log('tick'));
Promise.resolve().then(() => console.log('promise'));
setTimeout(() => console.log('timeout'));
setImmediate(() => console.log('immediate'));5
// tick, promise, timeout, immediate6
setImmediate against setTimeout(fn, 0) is the classic Node question, and the honest answer is that at
the top level the order is not deterministic — it depends on how long the process took to start.
setImmediatesetTimeout(fn, 0)
7
Inside an I/O callback it is deterministic:
8
fs.readFile('f', () => {
setTimeout(() => console.log('timeout'));
setImmediate(() => console.log('immediate'));
});
// immediate always first9
Because the I/O callback runs in the poll phase, and check comes straight after it, while timers waits
for the next turn.
checktimers
10
process.nextTick can starve the loop.
function spin() { process.nextTick(spin); }process.nextTick
11
The nextTick queue is drained completely before the loop continues, so this blocks everything — the same
hazard as recursive microtasks in the browser, with a higher priority.
12
The browser comparison.
browser one macrotask queue, plus rendering
Node several phases, no rendering, plus nextTick13
Which is why an ordering that holds in one does not always hold in the other, and why code depending on
exact interleaving is fragile in both.
14
The practical takeaway. Use setImmediate to yield after I/O, setTimeout for a real delay, and
process.nextTick sparingly — it is for running something before the loop continues, not for deferring
work.
setImmediatesetTimeoutprocess.nextTick
15
What the interviewer is probing.1. "Name the main phases." Probing: the model. Stalls: "One queue." Moves up: timers,
pending callbacks, poll, check and close — with microtasks drained between each.
16
2. "setImmediate or setTimeout(fn, 0) — which runs first?" Probing: the subtlety. Stalls:
"setTimeout." Moves up: inside an I/O callback setImmediate runs first because the check phase
follows poll; at the top level the order is not deterministic.
17
3. "Where does process.nextTick sit?" Probing: the priority. Stalls: "With promises." *Moves
up:* its own queue drained before promise microtasks, which is why overusing it can starve
everything else.
18
4. "How do you avoid blocking the loop in Node?" Probing: the options. Stalls: "Use async."
Moves up: worker threads for CPU work, streaming rather than buffering, and chunking long loops —
async alone does not create parallelism.