In this lesson, you will learn how to identify when a deluge of micro-tasks—specifically process.nextTick or Promise callbacks—monopolizes the CPU and prevents the Node.js event loop from advancing to its next phase.
The Micro-Task Trap
In Node.js, micro-tasks reside in a special queue that is drained immediately after the current operation completes and before the event loop moves to the next phase (like polling for I/O or timers). If your application recursively schedules micro-tasks—for example, a Promise chain that recursively calls itself—the event loop remains "trapped" in the micro-task queue indefinitely.
Because the event loop cannot advance, I/O callbacks, timers, and even setImmediate calls remain pending. This manifests as a complete cessation of responsiveness.
Identifying the Bottleneck
When you analyze a CPU profile (like a Flame Graph) of a starved event loop, you will notice a distinct absence of "Event Loop" phases (like uv__io_poll or timers) at the top of the stack. Instead, you will see a deep, narrow stack dominated by your own Promise handlers or process.nextTick callbacks.
Consider this problematic pattern:
function recursiveTask() {
Promise.resolve().then(recursiveTask);
}
recursiveTask();
In a production flame graph, this appears as a "tower" that consumes 100% of the sample time, preventing the stack from ever returning to the event loop's base iteration. While the CPU usage is high, the "Total Time" spent in user-land code is actually starving the platform of the ability to handle incoming network requests or timeouts.
Profiling Tips
To detect this, look for:
- Vanishing I/O: If your
uv__io_pollortimersblocks are completely missing from your Flame Graph despite having active network load. - Infinite Recursion: Functions that appear at the bottom of the flame graph but have identical stacks repeating continuously in the "Self Time" analysis.
- Queue Bloat: If you suspect micro-task starvation, check the length of the micro-task queue, as excessive chaining creates a synchronous "blocking" effect that mimics long-running JIT-optimized loops.
Key Takeaway: Micro-tasks are executed before the event loop advances; excessive recursive scheduling of Promises or nextTick will starve the event loop, effectively blocking all I/O and timer functionality.