When process.nextTick Beats setImmediate and Why That Can Starve I/O
process.nextTick runs as a microtask before I/O and setImmediate, which runs in the check phase. Using nextTick recursively can starve I/O callbacks. Here is how the ordering works and when to choose each.
13 Apr 2026, 12:39 UTC

You schedule a cleanup with process.nextTick and a deferral with setImmediate in the same turn of code, expecting the setImmediate work to run first. It does not. The nextTick callback runs immediately after the current operation finishes, before any I/O callbacks and before the check phase where setImmediate lives. That ordering is guaranteed in Node.js supported releases, and it is the source of subtle inversion bugs and I/O starvation when nextTick is used recursively.
The ordering problem in practice
process.nextTick is a microtask queue that drains after the current operation completes but before the event loop continues to the next phase. setImmediate is scheduled for the check phase, which runs after the poll phase has processed I/O callbacks.
When both are queued in the same tick, nextTick always runs first. If a nextTick handler queues another nextTick, the loop never reaches poll or check. I/O callbacks pile up while microtasks keep draining. This is not a bug; it is the defined contract.
Where each callback lives in the loop
Node.js event loop phases relevant here are timers, pending callbacks, poll, check, and close. Microtasks are not a phase but are drained at specific points between phases.
process.nextTick
nextTick callbacks are stored in an internal queue separate from Promise microtasks. The queue is drained after the current JavaScript stack empties and before the event loop proceeds. Because it runs before I/O callbacks, it is useful for internal housekeeping that must complete before user code observes a state change, but dangerous for deferring work.
setImmediate
setImmediate callbacks are placed in the check phase queue. The check phase runs after poll has had a chance to process readable and writable I/O events. This makes setImmediate suitable for deferring CPU-bound follow-up work so pending I/O can proceed first.
Worker threads and clusters each have their own event loop instance. Scheduling guarantees do not cross process or thread boundaries unless you explicitly coordinate via parentPort messaging.
Worked example to observe ordering
Create a file nexttick-vs-setimmediate.js in a project directory you can read and execute. No elevated permissions are required. Run it with a supported Node.js release using:
node nexttick-vs-setimmediate.jsThe script queues both callbacks from the same synchronous turn and logs a marker before and after each callback.
// nexttick-vs-setimmediate.js
console.log('sync start');
process.nextTick(() => {
console.log('nextTick');
});
setImmediate(() => {
console.log('setImmediate');
});
console.log('sync end');
Expected check: the log order should be sync start, sync end, nextTick, then setImmediate. The nextTick entry appears before setImmediate because microtask draining happens before the loop advances to the check phase.
Risk: adding console.log inside the callbacks is fine for observation, but in production code avoid logging in hot paths. The script does not change state, so no rollback is needed.
Limitation: this ordering is per event loop tick. If you queue setImmediate from inside a nextTick handler, the setImmediate will still be deferred to the next check phase, not run immediately.
Trade-off and starvation
The power of nextTick is also its hazard. Recursive nextTick queuing can starve I/O callbacks indefinitely because the microtask queue is drained to completion before poll is revisited. A common pattern that causes this is:
function work() {
process.nextTick(work);
}
work();
This loop never yields to I/O. setImmediate would yield, because each iteration requires a full loop pass through poll and check.
Practical way to check for starvation in your own code: add a diagnostic timer that logs if I/O callbacks are delayed beyond an expected latency, or use an external observer such as an HTTP request that should be handled promptly while the loop is busy.
When to use which
Use process.nextTick for internal, synchronous-looking cleanup that must run before the current call stack is considered complete, and only when you will not recurse.
Use setImmediate to yield to I/O, to break up CPU work, or when you explicitly want the next check phase.
Avoid mixing them in tight loops. If you need predictable interleaving with I/O, prefer setImmediate or queueMicrotask for Promise-based microtasks, and keep nextTick usage shallow and non-recursive.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.