Course contentsShow
← Node.js
Lesson 1929 of 1,98017. Performance Tuning and ProfilingFree lesson

Detecting CPU Starvation in Micro-Tasks

You will find micro-tasks that consume excessive CPU time before the event loop can progress.

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:

  1. Vanishing I/O: If your uv__io_poll or timers blocks are completely missing from your Flame Graph despite having active network load.
  2. Infinite Recursion: Functions that appear at the bottom of the flame graph but have identical stacks repeating continuously in the "Self Time" analysis.
  3. 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.