NodeJSsetImmediate & process.nextTick

setImmediate & process.nextTick

Beyond setTimeout, Node has two special scheduling functions — setImmediate and process.nextTick — plus the standard queueMicrotask. They differ in exactly when their callbacks run relative to the event loop. The names are famously misleading (nextTick fires sooner than setImmediate!), and confusing them causes some of the trickiest Node bugs. Let's make the ordering exact.

The four schedulers at a glance

Scheduler

Runs

Priority

process.nextTick(fn)

After the current operation, before the loop continues

Highest

queueMicrotask(fn) / .then

Right after the nextTick queue drains

High

setImmediate(fn)

In the check phase, after I/O callbacks

Per-loop

setTimeout(fn, 0)

In the timers phase of the next iteration

Per-loop

Two queues vs the loop phases
The first two — nextTick and microtasks — are not event-loop phases at all. They are **queues that drain between every operation and between every phase**. The last two are tied to specific loop phases. That structural difference is the whole story.
Ordering, demonstrated

order.js

JS
console.log('1: sync')

setTimeout(() => console.log('5: setTimeout 0'), 0)
setImmediate(() => console.log('6: setImmediate'))

Promise.resolve().then(() => console.log('4: promise microtask'))
process.nextTick(() => console.log('3: nextTick'))

console.log('2: sync end')
1: sync
2: sync end
3: nextTick
4: promise microtask
5: setTimeout 0
6: setImmediate

Synchronous code runs first (1, 2). Then, before the loop advances to any phase, Node drains all nextTick callbacks (3), then all microtasks (4). Only then does the loop enter its phases: timers (5), then check (6).

The drain order, precisely

What happens between operations

Text
run current operation
  └─ drain ENTIRE process.nextTick queue
       └─ drain ENTIRE microtask (Promise) queue
            └─ advance to the next event-loop phase
                 └─ (repeat the drain after each callback)
Both queues drain fully, and can refill
Node empties the nextTick queue *completely*, then the microtask queue *completely* — and if draining one adds items to the other, it keeps going until both are empty before the loop moves on. This is what makes runaway recursion in these queues so dangerous.
The danger of process.nextTick
Recursive nextTick starves the loop
Because the nextTick queue must be fully drained *before* the loop can advance, scheduling `nextTick` from within a `nextTick` callback — recursively — means the loop **never** reaches the I/O or timer phases. Pending I/O, timers, and incoming requests are starved indefinitely. The process pegs a CPU core while appearing to do nothing.

DON'T do this

JS
function loop() {
  process.nextTick(loop)   // starves all I/O and timers forever
}
loop()
setImmediate is the safe yield
To break a long job into chunks while *letting I/O proceed between chunks*, use `setImmediate` — it schedules the continuation for the check phase, so the poll phase still runs and serves requests in between. Recursive `setImmediate` does not starve the loop the way recursive `nextTick` does.
setImmediate vs setTimeout(0)

At the top level their order is non-deterministic — it depends on how long process startup took relative to the 1ms timer floor:

JS
// Top level — order can vary run to run:
setTimeout(() => console.log('timeout'), 0)
setImmediate(() => console.log('immediate'))

But inside an I/O callback the order is guaranteed: setImmediate always wins, because the check phase comes immediately after the poll (I/O) phase, whereas timers must wait for the next full turn:

JS
import { readFile } from 'node:fs'

readFile(__filename, () => {
  setTimeout(() => console.log('timeout'), 0)
  setImmediate(() => console.log('immediate'))
})
immediate
timeout
When to use which
  • setImmediate — "run after the current I/O, but soon." The safe choice for breaking up CPU work and yielding to the loop.

  • process.nextTick — run cleanup or emit an event before anything else, e.g. to make a sometimes-sync API consistently async. Use sparingly and never recurse.

  • queueMicrotask — schedule a microtask without manufacturing a resolved promise; the standards-based choice.

  • setTimeout(fn, 0) — defer to the next timers phase; slightly later than setImmediate inside I/O callbacks.

Section complete
That wraps up Fundamentals. Next major topic: [Introduction to Modules](/nodejs/modules-intro) — how Node organizes and shares code.