# The Node.js event loop

Node runs a synchronous script first, then repeatedly handles work scheduled by timers, I/O, and other APIs. JavaScript callbacks normally execute one at a time; libuv coordinates I/O so a program can wait for several operations without blocking on each one. A busy synchronous loop still blocks every other JavaScript callback.

# Phases

The official Node.js event-loop guide (opens new window) uses this conceptual phase cycle:

Phase What it handles Example
timers Timer callbacks whose threshold has elapsed setTimeout, setInterval
pending callbacks Selected I/O/system callbacks deferred to another iteration Some TCP errors
idle, prepare Internal setup No user callbacks to schedule here
poll New I/O and most ready I/O callbacks; may wait for work fs.readFile callback
check Callbacks queued for the check phase setImmediate
close callbacks Some abrupt/explicit close notifications socket.on('close', ...)

Each phase has its own responsibilities and usually a callback queue. A phase may have no callbacks; the loop can wait in poll and exits when nothing keeps the process alive. process.nextTick() and Promise/queueMicrotask() callbacks are not extra libuv phases: they run at microtask checkpoints around callback execution. Typically the next-tick queue is drained before Promise microtasks in CommonJS callbacks; ESM top-level evaluation has different initialization timing, so do not rely on this as a universal cross-context ordering rule. Recursive next-tick scheduling can starve I/O.

# Follow one illustrative trace

This CommonJS example schedules the timer and immediate inside the file callback, avoiding the non-guaranteed top-level race between them:

const fs = require('node:fs');

console.log('start');
fs.readFile(__filename, () => {
  console.log('I/O');
  process.nextTick(() => console.log('nextTick'));
  Promise.resolve().then(() => console.log('promise'));
  setImmediate(() => console.log('immediate'));
  setTimeout(() => console.log('timer'), 0);
});
console.log('end');

The following guided illustration, not a live Node scheduler, traces one plausible run: synchronous output, poll callback, nextTick and Promise continuations, check, then a timer on a later loop pass. The internal and empty phases are shown so they are not mistaken for missing phases.

If the interactive controls are unavailable, trace the sample manually: start → end → I/O → nextTick → promise → immediate → timer (the last two belong to check and a later timers pass in the illustrated run).

# Ordering that matters

  • setTimeout(fn, 0) gives a minimum timer threshold, not an immediate interrupt. A long synchronous task delays it regardless of how small the delay is.
  • At top level, setTimeout(fn, 0) versus setImmediate(fn) can vary by context and timing. Inside an I/O callback, setImmediate usually runs in check before the next timer pass.
  • Starting with Node 20 / libuv 1.45, timers are processed after poll rather than both before and after poll as in older versions. A simplified phase chart describes which queues receive callbacks, not a promise of identical output on every Node release.
  • await/Promise continuations are microtasks, not poll or check callbacks. An async filesystem operation completes outside the JavaScript call stack; its callback resumes on the loop.

Try running the sample on your installed Node version, predict the output before each run, and compare it with the visualizer. Do not use a long busy loop as a performance benchmark.