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 |
|---|---|---|
| After the current operation, before the loop continues | Highest |
| Right after the nextTick queue drains | High |
| In the check phase, after I/O callbacks | Per-loop |
| In the timers phase of the next iteration | Per-loop |
Ordering, demonstrated
order.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
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)The danger of process.nextTick
DON'T do this
function loop() {
process.nextTick(loop) // starves all I/O and timers forever
}
loop()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:
// 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:
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 thansetImmediateinside I/O callbacks.