Does process.nextTick starve the event loop poll phase during heavy microtask execution?
0 reputation · 14 Oct 2025, 14:41 UTC
0 reputation · 14 Oct 2025, 14:41 UTC
In Node.js (v18+), the event loop manages various phases including timers, poll, and check. While process.nextTick() and Promise microtasks are processed immediately after the current operation, there is a concern regarding the priority of these queues over the actual event loop phases.
If a recursive chain of process.nextTick() calls or a high volume of resolved Promises is maintained, the runtime may prioritize exhausting these queues before advancing to the poll phase. This behavior potentially prevents the application from processing new I/O events or executing setTimeout callbacks.
What is the specific threshold or mechanism that prevents the microtask queue from completely blocking the poll phase indefinitely? Does setImmediate() provide a guaranteed way to break a process.nextTick() recursion loop?
34775 reputation · 14 Oct 2025, 21:25 UTC
Yes. In Node.js a recursive process.nextTick() chain can keep the event loop from ever leaving the nextTick queue, which effectively blocks the poll phase and prevents I/O and timer callbacks from running.
There is no internal “gas limit” or threshold that automatically yields to the poll phase. The loop will continue to empty the nextTick queue until it is empty, regardless of how many microtasks (Promises) are queued. When process.nextTick is used recursively, the loop never reaches the poll phase, so new I/O events are never processed.
Promises are processed after the nextTick queue, but they share the same starvation risk if they are queued recursively. However, process.nextTick has higher priority and will always be drained first.
nextTick queue completely before moving to the next event‑loop phase.nextTick can run indefinitely.nextTick but can also block if queued recursively.setImmediate() places it in the check phase, which requires the loop to run through poll first.setImmediate() guarantees that the poll phase will execute at least once before the queued callback runs.Create a minimal script that recurses with process.nextTick and also starts a setInterval(0) to observe I/O.
let count = 0;
function tick() {
count++;
if (count < 10) process.nextTick(tick);
}
process.nextTick(tick);
setInterval(() => console.log('tick'), 0);
Run it. You will see the interval callback never fires because the nextTick recursion blocks the poll phase.
Replace process.nextTick(tick) with setImmediate(tick) and rerun. The interval will now fire, proving that the poll phase ran.
To tailor recommendations for your specific code, could you share whether the recursion depth is bounded or if you’re also queuing large numbers of resolved Promises in the same loop?
In practice, avoid deep or unbounded process.nextTick recursion. Use setImmediate or a small timeout to give the poll phase a chance to run, especially when you need to handle I/O or timers concurrently.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.