The Node.js event loop: phases, setImmediate and process.nextTick
Node.js runs its event loop in phases. Learn the phase order, why setImmediate beats setTimeout inside I/O callbacks, and where nextTick fits.
Comes up in: JavaScript, Backend design
Learn#
Rohan’s Node.js server reads a file, then uses setTimeout(fn, 0) to “run it next”. In tests, a setImmediate callback keeps running first. He swaps them and the order changes again. Node.js does not have one queue. It has a loop with stages.
The loop comes from libuv, the C library under Node.js. Each turn passes through fixed phases, and each phase has its own queue.
In order: pending callbacks, idle/prepare (internal), poll (wait for I/O and run its callbacks), check (setImmediate), close callbacks, then timers. Since Node.js 20, timers run after poll, plus once before the loop starts.
So inside an I/O callback — which runs in poll — setImmediate always runs before a 0 ms timeout: check comes first.
Between every callback, Node.js empties two queues: first process.nextTick, then promise microtasks.
import { readFile } from "node:fs";
import { fileURLToPath } from "node:url";
readFile(fileURLToPath(import.meta.url), () => {
setTimeout(() => console.log("4 timeout"), 0);
setImmediate(() => console.log("3 immediate"));
Promise.resolve().then(() => console.log("2 promise"));
process.nextTick(() => console.log("1 nextTick"));
});Output (verified)
1 nextTick 2 promise 3 immediate 4 timeout
Call stack
- readFile callback
Microtasks
- nextTick: 1
- promise: 2
Task queue
- check: immediate
- timers: timeout
Console
empty
Poll phase: the file has been read, so its callback runs. It schedules four things.
Read the steps as text
- Poll phase: the file has been read, so its callback runs. It schedules four things.
- The callback returns. Node empties the nextTick queue first, then promise microtasks.
- After poll comes the check phase: setImmediate callbacks run.
- Then close callbacks, then timers. The 0 ms timeout finally runs.
Predict#
Predict first
At the top level of your main file (not inside any callback), you call setTimeout(a, 0) and then setImmediate(b). Which runs first?
Outside an I/O callback, the first timers check happens before the loop starts, and whether 1 ms has passed by then depends on how fast the process started. The Node.js docs say the order varies. Inside an I/O callback the answer would be b, every time — that is the case people remember and wrongly apply everywhere.
Interview angle#
Say it like this — about 30 seconds
“Node.js uses libuv’s event loop, which runs in phases: pending callbacks, poll for I/O, check for setImmediate, close callbacks, and timers — since Node 20 timers run after poll. Inside an I/O callback, setImmediate always beats setTimeout zero. Between callbacks Node drains process.nextTick first, then promise microtasks. So nextTick is the most urgent, but it can starve I/O. Blocking work like file system and crypto calls runs on libuv’s thread pool.”
Follow-up List the phases of the Node.js event loop.
Follow-up nextTick, setImmediate or setTimeout(fn, 0) — when would you use each?
Follow-up Does nextTick always run before promise callbacks?
Practice#
On your own
Write order(kinds, where) that predicts Node.js ordering for the four kinds of callback. Return null when Node.js does not promise an order.
Done when
- Inside I/O: nextTick, then promises, then immediates, then timeouts.
- At the top of an ES module: promises before nextTick.
- Timeout and immediate together at module top level return
null.
Remember#
Try to answer each card before you open it. Then add them to your reviews.
Name the Node.js event loop phases in order.
Pending callbacks, idle/prepare, poll, check, close callbacks, timers.
Inside an I/O callback, which runs first: setImmediate or setTimeout(fn, 0)?
setImmediate, always: check comes before timers.
Inside a callback, which queue does Node.js drain first: nextTick or promises?
nextTick, then promise microtasks.
Which Node.js work runs on libuv's thread pool?
Blocking work such as most fs calls, dns.lookup, some crypto and zlib.
Go deeper
Four layers. Stop at the one your interview needs; each ends with a quick check.
The core ideaEvery round
Node.js loops through phases, each with its own queue. I/O callbacks run in poll, setImmediate in check, timers in the timers phase. Between any two callbacks, nextTick callbacks run, then promise callbacks.
Check yourself: Where do setImmediate callbacks run?
In the check phase, right after poll.
How it works underneathSenior rounds
The Node.js guide lists the phases and the libuv 1.45.0 change (Node.js 20): timers now run after poll in each turn Source: Node.js: The event loop, timers, and process.nextTick(). process.nextTick is not a phase; its queue is drained after the current operation, whatever the phase.
Check yourself: What changed for timers in Node.js 20?
Timers run after the poll phase instead of before it, plus once before the loop starts.
Edge cases and trapsWhere people slip
At the top level of an ES module, the order flips:
import { nextTick } from "node:process";
Promise.resolve().then(() => console.log("promise"));
nextTick(() => console.log("nextTick"));Output (verified)
promise nextTick
The Node.js process docs describe this difference between ES modules and CommonJS Source: Node.js: process.nextTick(). Recursive nextTick calls can starve I/O, because the loop never reaches poll.
Check yourself: Why do promises beat nextTick at the top of an ES module?
The module is evaluated inside a promise job, so its microtasks drain before Node gets to the nextTick queue.
In productionStaff-level answers
libuv’s thread pool runs blocking work: most fs calls, dns.lookup, and CPU-heavy crypto and zlib calls. It has 4 threads by default, set by UV_THREADPOOL_SIZE before startup Source: libuv: Thread pool work scheduling. Network sockets do not use it. Your JavaScript still runs on one thread, so CPU work in a handler blocks every request — use worker threads for that.
Check yourself: Four slow crypto.pbkdf2 calls are running. Why might a fifth wait?
libuv's thread pool has 4 threads by default, so the fifth waits for a free thread.
Finished the five steps? Mark it, and its cards join your reviews.
Last checked 2026-10-31. Spot a mistake? Use “Report an error” below — fixes are listed on the corrections page.
Interview questions this answers
- What is the difference between
process.nextTick,setImmediateandsetTimeout(fn, 0)in Node? - List the phases of the Node.js event loop.