The Node.js Event Loop
How one thread runs thousands of things at once — the phases, the queues, and the exact order your callbacks fire.
Node runs your JavaScript on a single thread — yet it can juggle thousands of network connections without breaking a sweat. The trick is the event loop: instead of waiting for slow work (a timer, a file read, a network response), Node hands that work off, keeps running, and comes back to the result later via a callback.
Understanding the loop is the difference between guessing what order things run in and knowing. Use the interactive visualizer below to build a program and watch it run, one honest step at a time.
#One thread, never blocking
Your JavaScript runs on one call stack. If you block it with slow, synchronous work, everything stops — no other requests, no timers, nothing. So Node's whole design is about never blocking: slow operations (I/O, timers) are delegated to the system (via a C library called libuv) and their callbacks are queued to run later, when the stack is free.
Think of a barista
A good barista doesn't stand and watch one espresso pour before taking the next order. They start the pour, take three more orders, steam some milk, then come back when the shot is ready. One person (one thread), lots of work in flight — because nothing blocks.
#The phases of the loop
Each turn of the loop (a tick) moves through a fixed set of phases. Each phase has its own queue of callbacks to run:
┌───────────────────────────┐
│ timers setTimeout / setInterval callbacks
│ pending some deferred system callbacks
│ poll retrieve I/O events; run I/O callbacks
│ check setImmediate callbacks
│ close 'close' event callbacks
└───────────────────────────┘
↑ repeat every tick ↑The two you'll reason about most are timers (where setTimeout(cb, 0) callbacks land) and check (where setImmediate(cb) callbacks land).
#Microtasks jump the queue
Two special queues are drained between every phase (and after every callback), before the loop moves on:
- `process.nextTick` callbacks — highest priority, run first.
- Promise callbacks (
.then/awaitcontinuations) — the microtask queue.
Because they drain before the next phase, a promise or nextTick always runs before a setTimeout that was scheduled at the same time.
`setTimeout(fn, 0)` is not immediate
A 0ms timeout doesn't run now — it runs in the timers phase of a future tick, after the current synchronous code finishes and after all microtasks drain. "0" just means "as soon as the loop can get to the timers phase."
console.log('1: sync');
setTimeout(() => console.log('2: timeout'), 0);
setImmediate(() => console.log('3: immediate'));
Promise.resolve().then(() => console.log('4: promise'));
process.nextTick(() => console.log('5: nextTick'));
console.log('6: sync');Read the output top to bottom: all synchronous code first (1, 6), then the microtasks with nextTick ahead of promises (5, 4), then the loop's phases — timers (2) and finally check (3). The visualizer below animates exactly this.
Which callback runs FIRST after the synchronous code finishes?
Why doesn't setTimeout(fn, 0) run immediately?
#Why it matters
This model is why Node is great at I/O-heavy work (servers, APIs, proxies) and why one blocking loop or a giant synchronous computation can freeze an entire server. Keep the loop free: do slow work asynchronously, and never block the thread.
Key takeaways
- Node runs your JS on one thread and stays responsive by never blocking — slow work is delegated and its callback is queued.
- Each tick runs phases in order: timers → pending → poll (I/O) → check (setImmediate) → close.
- Microtasks drain between every phase: process.nextTick first, then Promise callbacks — so they beat setTimeout(0).
- setTimeout(fn, 0) means 'as soon as the timers phase is reached', not 'immediately'.
- Blocking the loop with heavy synchronous work stalls every other request — keep it free.
1console.log("start")2setTimeout(() => console.log("timeout"), 0)3setImmediate(() => console.log("immediate"))4Promise.resolve().then(() => console.log("promise"))5process.nextTick(() => console.log("nextTick"))6console.log("end")
The script runs top to bottom on the call stack.
Build your own program with the buttons above, then press Play to watch the exact order things run.
What does this Node program print?
console.log('start');
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('end');process.nextTick vs a Promise — which prints first?
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));A developer expects 'done' to print immediately. Why doesn't it?
setTimeout(() => console.log('done'), 0);
for (let i = 0; i < 1e9; i++) {}
console.log('loop finished');Order these by when they run in a single Node program that schedules all of them at once.
synchronous console.log calls
setTimeout(…, 0) callbacks (timers phase)
Promise .then callbacks
process.nextTick callbacks
setImmediate callbacks (check phase)
Predict the console output of this program, then check with the visualizer:
``js console.log('A'); process.nextTick(() => console.log('B')); Promise.resolve().then(() => console.log('C')); setTimeout(() => console.log('D'), 0); console.log('E'); ``
Try it yourself — a starting point to build on:
# Write your solution here