NodeJSEvent Loop Phases

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

setTimeout / setInterval callbacks whose time elapsed

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

setImmediate callbacks

6

close callbacks

'close' events, e.g. socket.on('close')

The cycle

Text
        ┌───────────────┐
   ┌───►│    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
nextTick and promises run *between* phases, not in one
`process.nextTick` callbacks and promise microtasks are **not** a phase. They're drained completely after each phase finishes and before the next begins — and after each individual callback. So they always run *sooner* than you'd expect from the phase list. Order between the two: `process.nextTick` queue first, then the Promise microtask queue.

Priority between phase callbacks

Text
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):

Why a Node process can use ~0% CPU while 'running'
An idle server isn't spinning — it's parked in the poll phase, asleep at the OS level until a socket has data or a timer comes due. This is how one thread serves many connections without burning CPU: it does nothing until there's genuinely something to do.
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:

JS
setTimeout(() => console.log('timeout'), 0)
setImmediate(() => console.log('immediate'))
// Order here is NON-deterministic — could print either first
But inside an I/O callback, the order *is* guaranteed
Within an I/O callback (i.e. during the poll phase), `setImmediate` **always** runs before `setTimeout(0)` — because the loop goes poll → check (immediate) next, and only reaches timers on the following tick. This determinism is why `setImmediate` is the right "run right after this I/O" tool. Full breakdown in [setImmediate & process.nextTick](/nodejs/set-immediate).

JS
import { readFile } from 'node:fs'

readFile(__filename, () => {
  setTimeout(() => console.log('timeout'), 0)
  setImmediate(() => console.log('immediate'))
})
// Inside I/O, ALWAYS: immediate → timeout
immediate
timeout
A full-order example

JS
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.

Next
The micro/macro distinction deserves its own deep dive: [Microtasks vs Macrotasks](/nodejs/microtasks-macrotasks).