Discover the libuv phases, the real difference between setImmediate and setTimeout, and how to detect a blocked event loop.
Open this lesson in KodokonNode.js delegates its event loop to libuv, which organizes it into distinct phases: timers (setTimeout, setInterval), pending callbacks, idle/prepare (internal), poll (I/O: sockets, files), check (setImmediate), and close callbacks. On each tick, the loop walks through these phases in that order. A detail few developers know: between each callback, Node first drains the process.nextTick queue, then the promise microtasks - and this has been the case since Node 11, after every callback rather than only between phases.
import { readFile } from "node:fs";
setTimeout(() => console.log("timeout"), 0);
setImmediate(() => console.log("immediate"));
readFile(new URL(import.meta.url), () => {
setTimeout(() => console.log("io timeout"), 0);
setImmediate(() => console.log("io immediate"));
});When the script starts, the order between setTimeout(fn, 0) and setImmediate(fn) is non-deterministic: setTimeout(fn, 0) is actually clamped to 1 ms, and if the loop reaches the timers phase in less than a millisecond, the timer isn't eligible yet. Inside an I/O callback, however, you're in the poll phase: the check phase (setImmediate) always comes before returning to the timers phase. So io immediate is consistently printed before io timeout.
setTimeout(() => {
process.nextTick(() => console.log("nextTick"));
Promise.resolve().then(() => console.log("promise"));
queueMicrotask(() => console.log("microtask"));
console.log("sync");
}, 0);
// sync, nextTick, promise, microtaskA subtle pitfall: at the top level of an ES module, this order is reversed - promise microtasks run before process.nextTick. The reason: evaluating an ES module is itself executed as a promise job, so the microtask queue is already being drained when your synchronous code finishes. In CommonJS, nextTick keeps its priority even at the top level. Always verify this kind of ordering in the module format you actually deploy.
The event loop runs on a single thread: any long synchronous computation - JSON.parse of a large payload, a catastrophic regular expression, synchronous encryption - freezes all in-flight requests. In production, measure loop latency with monitorEventLoopDelay: a p99 above a few tens of milliseconds signals a stall. The following code deliberately triggers a stall and then measures it.
import { monitorEventLoopDelay }
from "node:perf_hooks";
const h = monitorEventLoopDelay({ resolution: 10 });
h.enable();
const end = Date.now() + 200;
while (Date.now() < end) {}
setTimeout(() => {
h.disable();
const ms = h.max / 1e6;
console.log(`max delay: ${ms.toFixed(1)} ms`);
}, 300);