CPCODELAB
Node.js

Timers & Event Loop Phases In Depth

10 min read

Timers & Event Loop Phases In Depth

The Node.js event loop has six phases executed in a fixed order. Each phase has a FIFO queue of callbacks. The loop drains each queue (or hits a system-specific limit) before moving to the next phase.

  1. timerssetTimeout and setInterval callbacks whose delay has elapsed
  2. pending callbacks — I/O errors deferred from the previous iteration
  3. idle / prepare — internal Node.js housekeeping only
  4. poll — retrieves new I/O events; blocks here if the queue is empty and no timers are pending
  5. checksetImmediate callbacks run here, after poll
  6. close callbacks — e.g. socket.on('close', ...)

Microtasks Run Between Every Phase

js
// Microtasks (Promises, queueMicrotask) drain completely
// between every phase transition.
setTimeout(() => console.log("setTimeout"), 0);
setImmediate(() => console.log("setImmediate"));
queueMicrotask(() => console.log("microtask"));
Promise.resolve().then(() => console.log("promise"));

// Typical output:
// microtask
// promise
// setTimeout   (or setImmediate first — order varies outside I/O callbacks)
// setImmediate

setTimeout vs setImmediate Inside I/O

When both setTimeout(fn, 0) and setImmediate(fn) are called inside an I/O callback, setImmediate always fires first because the I/O callback runs in the poll phase, and check (setImmediate) comes next — before the loop cycles back to timers.

js
const fs = require("fs");

fs.readFile(__filename, () => {
  setTimeout(() => console.log("setTimeout inside I/O"), 0);
  setImmediate(() => console.log("setImmediate inside I/O"));
});
// Always prints:
// setImmediate inside I/O
// setTimeout inside I/O

process.nextTick — Not a Phase

js
// process.nextTick fires before ANY I/O or timer callbacks,
// and before Promises — right after the current operation completes.
console.log("sync 1");
process.nextTick(() => console.log("nextTick"));
Promise.resolve().then(() => console.log("promise"));
console.log("sync 2");
// sync 1  ->  sync 2  ->  nextTick  ->  promise

Recursive process.nextTick calls can starve the event loop — the tick queue is drained completely before moving on, so an infinite nextTick chain blocks I/O forever. Prefer setImmediate for deferring work past the current operation.

Ready to test yourself?
Practice Node.js— quiz & coding exercises