Event Loop Phases
A single tick of the event loop is not one queue — it's a fixed sequence of phases, each draining its own kind of callback before moving to the next. Knowing the phases (and the two special micro-queues that interleave between them) is what lets you predict the exact order of timers, I/O callbacks, setImmediate, process.nextTick, and promises.
The six phases, in order
Phase | Runs | |
|---|---|---|
1 | timers |
|
2 | pending callbacks | Certain deferred system callbacks (e.g. some TCP errors) |
3 | idle, prepare | Internal use only |
4 | poll | Retrieve new I/O events; run most I/O callbacks; may block here |
5 | check |
|
6 | close callbacks |
|
The cycle
┌───────────────┐ ┌───►│ timers │ setTimeout / setInterval │ ├───────────────┤ │ │ pending cbs │ │ ├───────────────┤ │ │ idle, prepare │ (internal) │ ├───────────────┤ │ │ poll │◄─── waits for I/O if nothing else pending │ ├───────────────┤ │ │ check │ setImmediate │ ├───────────────┤ │ │ close cbs │ 'close' events │ └───────┬───────┘ └────────────┘ ★ Between EVERY phase: drain nextTick queue, then microtask (promise) queue
The two queues that cut the line
Priority between phase callbacks
After any callback / phase boundary, fully drain in this order: 1. process.nextTick queue (highest priority) 2. Promise microtask queue …only then move to the next phase's macrotasks
The poll phase is special
The poll phase is where Node spends most of its idle time. If there are no timers due and no setImmediate callbacks queued, the loop will block here waiting for incoming I/O — which is exactly what you want a server to do between requests (sleep cheaply, wake on a connection):
setTimeout(0) vs setImmediate
These two are a classic interview question. setImmediate fires in the check phase; setTimeout(fn, 0) fires in the timers phase of the next tick. From the top level their order is not guaranteed — it depends on timing nuances:
setTimeout(() => console.log('timeout'), 0)
setImmediate(() => console.log('immediate'))
// Order here is NON-deterministic — could print either firstimport { readFile } from 'node:fs'
readFile(__filename, () => {
setTimeout(() => console.log('timeout'), 0)
setImmediate(() => console.log('immediate'))
})
// Inside I/O, ALWAYS: immediate → timeoutimmediate timeout
A full-order example
console.log('A: sync')
setTimeout(() => console.log('B: timeout'), 0)
setImmediate(() => console.log('C: immediate'))
Promise.resolve().then(() => console.log('D: promise'))
process.nextTick(() => console.log('E: nextTick'))
console.log('F: sync')A: sync F: sync E: nextTick ← nextTick queue, before promises D: promise ← microtask queue B: timeout ← timers phase (or C/B may swap at top level) C: immediate ← check phase
Sync first, then nextTick, then promises, then the macrotask phases. The B/C order can vary at the top level per the nuance above.