Skip to main content

What is the difference between process.nextTick() and setImmediate()?

process.nextTick() schedules a callback right after the current call stack empties, before the event loop advances to any phase. setImmediate() schedules a callback for the check phase, which runs after the poll (I/O) phase completes.

Theory

TL;DR

  • The event loop has phases: timers, poll (I/O), check, close. nextTick fires between any two phases; setImmediate only fires in the check phase.
  • process.nextTick() uses a microtask queue that V8 drains completely before uv_run() starts the next phase. setImmediate() pushes a uv_check_t handler into libuv.
  • Recursive nextTick starves I/O. Recursive setImmediate does not.
  • Decision rule: need to run before any I/O? Use nextTick. Want to yield to I/O and timers? Use setImmediate.
  • Both fire after synchronous code. Their relative order is deterministic only when called from inside an I/O callback.

Quick example

js
setImmediate(() => console.log('I - setImmediate')); process.nextTick(() => console.log('N - nextTick')); Promise.resolve().then(() => console.log('P - Promise')); console.log('sync'); // Output (always): // sync // N - nextTick // P - Promise // I - setImmediate

nextTick fires first because V8 drains the microtask queue before the event loop advances. Promise.then() lives in that same queue but resolves after nextTick callbacks. setImmediate waits for the check phase.

Key difference

The gap between nextTick and setImmediate is not just timing, it is where in the engine the callback lives. nextTick callbacks sit in a dedicated queue that Node drains synchronously inside InternalCallbackScope, before uv_run() gets to do anything. setImmediate registers a uv_check_t handler in libuv, which only fires when the event loop reaches the check phase after polling for I/O. That is why nextTick can block I/O entirely if you recurse into it, and setImmediate cannot.

When to use

  • Run before any I/O in the current cycle: process.nextTick() (e.g., emit an event after a constructor returns so listeners can attach first).
  • Yield to pending I/O or timers: setImmediate() (e.g., defer post-response cleanup in an Express route handler).
  • Recursive polling or retry loops: always setImmediate. nextTick recursion at depth >1k blocks timers by 100ms or more.
  • Consistent ordering with Promise.then(): nextTick fires before Promise callbacks in the same microtask drain cycle.

Event loop priority

PriorityMechanismQueue / Phase
1 (highest)process.nextTick()Microtask (pre-loop)
2Promise.then()Microtask (pre-loop)
3setTimeout(fn, 0)Timers phase
4setImmediate()Check phase

How Node handles them internally

Node.js uses libuv for the event loop with phases in this order: timers, poll (I/O), check, close. setImmediate() registers a uv_check_t handler that libuv calls in the check phase via uv__run_check(), after uv__io_poll() finishes. process.nextTick() bypasses libuv entirely. V8 drains the nextTick queue inside InternalCallbackScope after any script or callback finishes, but before uv_run() continues to the next phase. That is why the Node.js docs describe nextTick as not technically part of the event loop.

One thing I noticed in production: wrapping process.exit() in nextTick after an I/O callback feels harmless but can cut off other pending I/O operations. setImmediate in that pattern is safer.

Common mistakes

Mistake 1: using nextTick for all async deferral

js
fs.readFile('file.txt', (err, data) => { process.nextTick(() => process.exit(0)); // exits before other I/O completes });

nextTick fires before the poll phase resumes, so other pending fs callbacks never run. Use setImmediate here.

Mistake 2: recursive nextTick in polling logic

js
function poll() { process.nextTick(poll); // never yields to the event loop } poll();

This starves all I/O and timers. At depth 1M+ the loop hangs indefinitely. Switch to setImmediate(poll) instead.

Mistake 3: expecting a nested setImmediate to fire before the outer one

js
process.nextTick(() => { setImmediate(() => console.log('nested setImmediate')); }); setImmediate(() => console.log('outer setImmediate')); // Output: // outer setImmediate // nested setImmediate

The setImmediate queued inside a nextTick callback misses the current check phase and runs in the next cycle. This surprises people who expect the nested one to run first.

Mistake 4: starvation in recursive error handling

js
setImmediate(() => console.log('I - setImmediate')); // queued let depth = 0; function recurse() { if (++depth > 5) return console.log('Done'); process.nextTick(recurse); } recurse(); // Output: // Done // I - setImmediate (delayed by 6 nextTick calls)

Six nextTick calls already delay setImmediate noticeably. At real scale (depth 1k+) you block timers by 100ms or more, which breaks timeout-sensitive code.

Real-world usage

  • Express route handlers: nextTick to release a DB connection after res.json(), before the poll phase restarts.
  • EventEmitter pattern: defer this.emit('ready') in a constructor with nextTick so callers can attach listeners synchronously before the event fires.
  • Hapi auth plugins: setImmediate after request processing so pending network callbacks are not blocked.
  • async_hooks: nextTick runs inside the current async context without phase delay, useful for before/after instrumentation hooks.
  • PM2 clustering: setImmediate for inter-process message deferral so worker I/O is not starved.

Follow-up questions

Q: Can setImmediate ever fire before process.nextTick()?
A: Not in Node.js >= 11 when both are called from top-level module code. In older versions or the REPL with an active timers phase, ordering was less predictable. In modern Node the microtask queue always drains before any phase.

Q: How do nextTick and Promise.then() relate to each other?
A: Both run in the microtask queue before the event loop advances. nextTick callbacks drain first, then Promise callbacks. So process.nextTick(cb) runs before Promise.resolve().then(cb) when both are queued in the same turn.

Q: What happens when nextTick is called from inside a setImmediate callback?
A: The nextTick callback runs immediately after the setImmediate callback returns, before the check phase moves to the next setImmediate in the queue. It does not wait for the next event loop iteration.

Q: (Senior) In libuv source, what handles setImmediate vs nextTick dispatch? Walk through execution order.
A: setImmediate registers a uv_check_t handle. libuv calls these in uv__run_check() inside uv_run(), after uv__io_poll(). nextTick never enters libuv: Node's InternalCallbackScope destructor flushes the nextTick queue via MicrotaskQueue::PerformCheckpointInternal() in V8 after each C++ callback boundary. So nextTick fires in the gap between any two libuv callbacks, not in a named phase.

Q: How would you diagnose nextTick starvation in production?
A: Use clinic.js doctor or run Node with --trace-event-categories v8. Clinic shows event loop lag over time. If loop delay spikes while CPU stays low, recursive nextTick is the usual suspect. Add a depth counter and switch to setImmediate after N iterations.

Examples

Basic: side-by-side ordering

js
console.log('1'); process.nextTick(() => console.log('2 - nextTick')); Promise.resolve().then(() => console.log('3 - Promise')); setImmediate(() => console.log('4 - setImmediate')); setTimeout(() => console.log('5 - setTimeout'), 0); console.log('6'); // Output (Node 18): // 1 // 6 // 2 - nextTick // 3 - Promise // 4 - setImmediate (or 5 then 4 - relative order of setImmediate and setTimeout is not guaranteed outside I/O)

nextTick and Promise drain the microtask queue synchronously before any loop phase. setImmediate and setTimeout(fn, 0) both run in loop phases, and their relative order outside an I/O callback is not guaranteed.

Intermediate: EventEmitter constructor pattern

js
const EventEmitter = require('events'); class MyEmitter extends EventEmitter { constructor() { super(); // nextTick defers emit until after the constructor returns, // giving the caller time to call .on('ready', handler) first process.nextTick(() => { this.emit('ready'); }); } } const emitter = new MyEmitter(); emitter.on('ready', () => console.log('ready!')); // Output: ready! // Without nextTick, 'ready' fires during the constructor and no listener is attached yet

This is probably the most common legitimate use of nextTick in library code. setImmediate would also work here, but nextTick guarantees the emit happens before any I/O in the same turn.

Senior: starvation in practice

js
setImmediate(() => console.log('setImmediate - should fire soon')); let depth = 0; function recurse() { if (++depth > 5) return console.log(`Done at depth ${depth}`); process.nextTick(recurse); } recurse(); // Output: // Done at depth 6 // setImmediate - should fire soon <-- delayed by all 6 nextTick calls // At depth 1_000_000, setImmediate and any timers // would be blocked for hundreds of milliseconds. // Fix: replace process.nextTick(recurse) with setImmediate(recurse)

The setImmediate callback queued before recurse() does not fire until the entire nextTick chain finishes. In a real scenario with a logger or health-check using recursive nextTick, this is how you quietly break timeout-sensitive operations.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?