Under The Hood
backend nodejs Node.js 18+ (libuv 1.x)

Node.js event loop: phases and the microtask queue

Last updated
Prerequisites:
JavaScript async basics
Understanding of callbacks and promises
  • nodejs
  • event-loop
  • libuv
  • microtasks
  • async
  • javascript

Read at your depth

The practical view

Node runs a single thread executing the event loop: at each iteration it processes phases in order — timers (setTimeout/setInterval callbacks ready), pending callbacks (deferred I/O), idle/prepare (internal), poll (I/O events, where it blocks waiting), check (setImmediate), close (close handlers). Between every phase — and after every callback — the microtask queue drains first: process.nextTick callbacks run before Promise.then callbacks. That means setTimeout(fn, 0) does not run before a resolved Promise: microtasks win. setImmediate vs setTimeout(0): setImmediate fires in the check phase of the current iteration; setTimeout(0) in the timers phase of the next — so inside an I/O callback, setImmediate reliably runs first.

Legacy vs modern

setTimeout(0) vs setImmediate vs microtasks ordering

Relying on timer ordering produces race-prone output; knowing the phases and microtask drain explains and fixes the ordering deterministically.

before → after
Timer soup
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
Promise.resolve().then(() => console.log('promise'));
Phase-aware ordering
queueMicrotask(() => console.log('microtask'));
setImmediate(() => console.log('immediate'));
setTimeout(() => console.log('timer'), 1);

Interview gotchas

Context

This question distinguishes candidates who memorized 'nextTick is bad for recursion' from those who understand the drain semantics of the microtask queue.

The mechanical answer

Both nextTick and promise microtasks are drained completely — until empty — before the loop proceeds to the next phase (nextTick runs before the promise microtask checkpoint, but both drain fully). If each callback re-queues itself, the queue never empties, so the timers/I/O phases never run: the process spins on microtasks forever and I/O events never get processed (CPU pegged, but no timers fire). process.nextTick is slightly worse because its queue is drained before promise microtasks in the same turn. The safe pattern is to bound the recursion, or convert to setImmediate when the loop must yield to I/O between steps.

Trap

The trap answer is 'recursive promises are fine because they yield'. Promise.then callbacks are still microtasks drained to completion; they do not yield to the event loop's I/O phases. Another trap: 'process.nextTick is just a faster setTimeout' — it is a microtask queue in the C++ layer, not a timer at all. Naming the drain-to-empty semantics for BOTH queues is the differentiator.

Under The Hood — a multi-depth technical interview hub.

Third-party licenses Press ⌘ K to search.