event-loopAnswer last reviewed July 2026

Say a script queues up four things - a process.nextTick, a callback on an already-resolved promise, a setTimeout with zero, and a setImmediate. What order will they fire in, and what actually decides that?

Works out the callback order from the queue structure itself, two microtask-class queues plus the loop phases, instead of reciting some memorized priority list

What an AI-prepared candidate might say

So the synchronous code always finishes first, then I believe it's process.nextTick, then the promise callback, and then the timer and the immediate at the end. Node gives process.nextTick the highest priority out of all the deferred stuff. It runs before promise microtasks, and both of these run before any timers. And setTimeout with zero doesn't literally run at zero, it gets scheduled for the next turn of the event loop, in the timers phase. setImmediate runs in a later phase, the check phase, which is the one handling immediates. So you'd see the sync code, then nextTick, then the promise, then the timeout and the immediate. Those last two can actually swap depending on process timing, it's a known quirk. Basically it's microtasks before macrotasks, with nextTick at the front of the microtasks.

Senior

I keep two structures in my head for this, because Node schedules from both of them. The first one is a pair of drain-to-empty queues - Node's own nextTick queue, and V8's microtask queue, which is where promise reactions and queueMicrotask callbacks land. After any callback returns (and the main script itself counts as a callback here), Node drains the whole nextTick queue, then the whole microtask queue, and only after that the loop gets to advance. This drain happens after every single callback. Between every timer, between every I/O event.

The second structure is the phases, which always run in one fixed order - timers, pending callbacks, poll, check, close. setTimeout entries fire in the timers phase once the loop's clock crosses their due time, and setImmediate fires in check. And here's the trap - Node clamps a timeout of 0 up to 1ms, and this clamp is what creates the main-script race. If the process takes more than a millisecond to reach the first timers phase (module loading or a first GC will easily do it), the timer is already due and it fires before check. On a fast start, the loop is there in under a millisecond and the immediate wins. Same binary, but different output from one run to the next.

Inside an I/O callback, the ambiguity just disappears. You're sitting in the poll phase, and check comes next, before the loop ever gets back to timers. So setImmediate wins there every single time.

And that gives you the answer to the puzzle. Sync code, then nextTick, then the promise callback, always in this order. Then either the timeout or the immediate, if the pair came from the main script. And if you queued that pair inside an I/O callback, then the immediate always comes first.

Staff

Three real-world things hang off this ordering. The first one is API contracts. Library code leans on nextTick to guarantee that events emit after the caller has finished attaching listeners. A constructor that emits synchronously loses events, and one that emits on a timer adds latency and reorders against other callbacks. Streams and a big chunk of core rely on this, which is why nextTick is still around despite its starvation risk. So when you're writing an emitter or a callback API, the place where you defer is part of the contract, because callers can observe whether their own synchronous code ran before your callback fired.

The second is test stability. Any assertion that depends on the main-script timeout-versus-immediate race will pass or fail based on startup timing, and it fails more under CI load, because slower startup pushes the 1ms clamp past the first timers phase. I've watched teams just keep hitting retry on those. The fix is structural - don't assert cross-phase ordering that you don't control, use fake timers for time-based logic, and when a test needs to run after all pending I/O, chain setImmediate instead of setTimeout(0).

The third is throughput. The 1ms clamp turns every setTimeout(0) hop into paid dead time, so a chunked batch job built on it spends most of its wall clock inside the timer machinery itself. setImmediate yields through the poll phase with no clamp at all. I've seen that one substitution take a migration script from hours down to minutes.

And when the ordering surprises you, just measure it. A ten-line script with timestamped logs will settle what some blog post from 2016 can't, and advice from the node --stack-size era ages badly. The queues are observable, so go and look at them directly.

Trap flags
  • People say 'nextTick runs on the next tick of the event loop' because the name itself begs you to say it. Actually it runs before the loop advances at all.
  • Watch out for 'setTimeout(0) fires immediately after the current code'. That skips right past the 1ms clamp and the timers phase.
  • A lot of candidates just recite one flat priority list. But the two queue classes drain at a different point than the phase callbacks, so a single ordered list can never be right.
Follow-up chain
Say a script queues up four things - a process.nextTick, a callback on an already-resolved promise, a setTimeout with zero, and a setImmediate. What order will they fire in, and what actually decides that? | NodeBook