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.
- timers —
setTimeoutandsetIntervalcallbacks whose delay has elapsed - pending callbacks — I/O errors deferred from the previous iteration
- idle / prepare — internal Node.js housekeeping only
- poll — retrieves new I/O events; blocks here if the queue is empty and no timers are pending
- check —
setImmediatecallbacks run here, after poll - 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)
// setImmediatesetTimeout 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/Oprocess.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 -> promiseRecursive 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