Case 13/15

Prefer reading? The full explanation

How setImmediate Runs

setImmediate vs setTimeout(0) · Node.js · CommonJS

Before this

How Node Runs It — the phase model and the two intermediate queues.

After this you can explain

Why setImmediate always beats setTimeout(0) inside an I/O callback, and why the same duel in a main module is a genuine coin flip.

console.log('1: start');

fs.readFile('data.txt', () => {
  console.log('3: I/O callback');

  setTimeout(() => console.log('5: setTimeout'), 0);
  setImmediate(() => console.log('4: setImmediate'));
});

console.log('2: script end');

Output: 1: start → 2: script end → 3: I/O callback → 4: setImmediate → 5: setTimeout

Two "run this next" APIs, two different phases

Node's event loop is a fixed cycle of phases: timers, pending callbacks, poll (where I/O completions are delivered), check, and close callbacks — and after every callback returns, Node drains the nextTick queue and then the microtask queue before touching the next callback. setTimeout(fn, 0) schedules its callback for the timers phase. setImmediate(fn) schedules its callback for the check phase. Neither means "now"; both mean "when the loop next passes my phase".

That single sentence replaces the folklore. Every ordering question about these two APIs reduces to: where is the loop standing when you schedule, and which of the two phases does it reach first from there?

Why the I/O callback makes it deterministic

The case above schedules both from inside an fs.readFile callback — which the poll phase delivers. From the poll phase, the loop's very next stop is the check phase: setImmediate runs in the same iteration. The freshly created 0ms timer, meanwhile, can only run when the loop wraps around to the next timers phase, a full lap away.

So inside any I/O callback, 4: setImmediate before 5: setTimeout is not probable — it is structural. The Node documentation states this guarantee explicitly, and this page's validator executes it on every build.

One honest footnote: in CI this case stubs the disk read with a timer, which delivers the callback in the timers phase instead of the poll phase. The guarantee is unaffected — from the timers phase, check still comes before the next timers phase — which is exactly why the stub is a legal shim: it swaps the mechanism and leaves the ordering to prove itself.

At top level, the same duel is a coin flip

Delete the readFile wrapper and run the two lines at the top of a module, and the output genuinely varies between runs. The reason is a detail most articles skip: Node clamps setTimeout(fn, 0) to a minimum of 1ms. When the loop begins its first iteration, it starts at the timers phase and asks: has 1ms elapsed since the timer was set?

On a fast startup the answer is no — the timers phase finds nothing, and the check phase runs setImmediate first. On a slower startup (busier machine, cold file cache, a debugger attached) the millisecond has passed, and the timer wins. Same file, both orders, neither a bug.

This is also a case study in what "verified" means on this site. A validator built on "run it once, assert the output" would be flaky here — or worse, would pass consistently while teaching an order that is not guaranteed. Worse still, the harness the other cases use starts every snippet from inside a callback, and from inside a callback this race cannot occur — an in-process check would happily report one fixed order forever. So this experiment is validated differently: the build launches it in thirty fresh Node processes, each a real module startup, and every observed output must be one of the two declared legal orders. The claim is not "the output is X"; the claim is "the output is never anything but X or Y" — which is the honest version.

And where does process.nextTick fit?

People compare process.nextTick and setImmediate because both names promise speed — but they are not two speeds of the same mechanism, they are different kinds of mechanism. setImmediate books a seat in a phase: its callback waits until the loop reaches check, after poll, this iteration. process.nextTick does not book a phase at all: its queue is drained the moment the current callback returns, before the event loop is allowed to continue anywhere — before other timers in the same phase, before I/O, before check.

So the honest ordering summary is: nextTick runs after the current operation, setImmediate runs after the current phase reaches check. The Node docs' own joke is that the names are swapped — process.nextTick fires more immediately than setImmediate does. The nextTick case on this site shows the further wrinkle that even that statement depends on the module system.

Practical rule: nextTick (or better, portable queueMicrotask) for "before anything else moves"; setImmediate for "after the I/O we already have is dealt with". They answer different questions, which is why replacing one with the other usually reorders a program rather than just re-timing it.

Which one should you actually use?

To defer work until after the current I/O batch: setImmediate. It is the only deferral primitive that yields to the poll phase between chunks, which is what keeps a busy server accepting connections (see the starvation example below). To defer within the current phase, before the loop moves at all: queueMicrotask — portable, standard, and drained before any phase transition.

setTimeout(fn, 0) in Node is almost always the wrong tool: it costs a 1ms clamp, lands in a phase you rarely mean, and its order against everything else depends on startup timing. If you find one in a Node codebase, it usually marks the spot where someone ported browser folklore.

Reading about it is one thing. Watching it run is another.

↑ Run it step by step

In the wild

Where this rule bites real code

The "flaky" test that only fails on CI

Symptom
A test asserts that a setTimeout(fn, 0) callback runs before a setImmediate callback. It passes locally for weeks, then fails intermittently on the slower CI box.
Why
The duel was happening at top level, where the order is a race against process startup: Node clamps the 0ms timer to 1ms, and whether that has elapsed when the loop first reaches the timers phase varies run to run.
Fix
Never assert an order between a timer and an immediate at top level. Move the work into one mechanism, or sequence it explicitly with await.

Chunked processing that starves I/O anyway

Symptom
A loop processes a huge array in chunks, deferring each chunk with process.nextTick to "let I/O breathe". The server still refuses connections during the run.
Why
The nextTick queue is drained COMPLETELY before the loop advances — chunking with nextTick never yields to the poll phase, so no I/O is serviced. It is starvation with extra steps.
Fix
Defer each chunk with setImmediate instead: the check phase runs once per loop iteration, so the poll phase — and your sockets — get a turn between chunks.

Environment

Node.js · CommonJS

CI verified

Node.js v22

fs.readFile substituted with a timer-backed stub (mechanism, not ordering — the callback-context guarantee holds in both phases); the main-module experiment is validated in 30 fresh Node processes per build — real module startups, since the race cannot occur inside a callback — and every output must be one of the two declared orders

Last reviewed

4 August 2026

The animation on this page is a teaching model, not a diagram of browser internals. Real engines use several task queues, optimise aggressively, and do plenty of work these panels do not show. What is guaranteed is the observable ordering: the stated output is produced by actually executing the code in CI, on the runtime named above. Browser-specific behaviour follows the HTML Standard and is not executed in a real browser by CI. How this is verified →