All topics
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 callbacks
2
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 phase
4
    process.nextTick(() => console.log('tick'));
    Promise.resolve().then(() => console.log('promise'));
    setTimeout(() => console.log('timeout'));
    setImmediate(() => console.log('immediate'));
5
    // tick, promise, timeout, immediate
6
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 first
9
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 nextTick
13
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.