How to Troubleshoot Promise Microtask Flood in Node.js

Production issues in Node.js applications can be challenging to diagnose, especially when different problems exhibit similar symptoms. To make troubleshooting easier, we’re introducing a Node.js Production Troubleshooting Series, where each article focuses on one specific production issue, explores its underlying causes, and demonstrates a practical, step-by-step approach to identifying the root cause.

Microtask Flood is one of those production issues that looks like a hung process, even when there is no obvious infinite loop. This usually happens when code keeps scheduling more microtasks using `Promise.then()` or `queueMicrotask()`. One microTask runs, schedules another, and the cycle continues. Because Node.js keeps processing microtasks before moving on to timers and other callbacks, HTTP requests can end up waiting longer than expected.

In this blog, we’ll walk through how a Promise microtask flood shows up in a Node.js application, what typically causes it, and how to trace it back to the exact root cause. We’ll also simulate the issue in a sample application, capture diagnostic data as it happens, and analyze it step by step to pinpoint what’s going wrong.

What is a ‘Microtask Flood’ in Node.js?

Node.js processes Promise callbacks and queueMicrotask() work before moving back to timers and I/O. Normally, this happens quickly and isn’t a problem.

The problem starts when each microtask keeps scheduling another microtask. The queue keeps growing, and the event loop gets less time to handle other work. As a result, timers, I/O, and HTTP requests can be delayed. A short delay may go unnoticed, but a longer one can make the application feel stuck.

What Causes a ‘Microtask Flood’ in Node.js?

Here is the usual list of causes for the ‘Microtask Flood’ problem.

  1. Recursive Promise.then(): A then-handler schedules another then, which schedules another, with no bound and no setTimeout yield.
  2. queueMicrotask loops: The same pattern with queueMicrotask instead of Promise.resolve().then().
  3. Unbounded batches: Walking a large list by microtasking one item at a time so I/O never gets a turn.
  4. Anonymous callbacks: Arrow functions show up as (anonymous) in Method Profiling. You can see the CPU, but not the call site.
  5. Mistaking microtasks for timers: Using Promise.then as a general “defer this” API. setTimeout and setImmediate run later and do not starve the microtask queue the same way.

How to Simulate Microtask Flood in Node.js

To understand how a Microtask Flood appears in the diagnostic data, let’s reproduce the issue using a sample Node.js application.

The following program keeps calling a named function, `microtaskFloodStep`, until about 320 ms have passed. Once that time is reached, it uses `setTimeout()` to give the event loop a chance to continue. We gave the function a name on purpose. This makes it easier to identify in the CPU profile. If we used an anonymous arrow function instead, it would appear as (anonymous), making the profile harder to read.

``` JS Code (microtask-flood.js)
function enqueueMicrotaskFloodStep(state) {
Promise.resolve().then(function microtaskFloodStep() {
if (!state.running) return;
for (let i = 0; i < 200; i++) {
state.n++;
}
if (Date.now() - state.startedAt < state.stallMs) {
enqueueMicrotaskFloodStep(state);
return;
}
setTimeout(function microtaskFloodYield() {
state.startedAt = Date.now();
enqueueMicrotaskFloodStep(state);
}, 0);
});
}
class MicrotaskFloodDemo {
constructor() {
this.isRunning = false;
this.state = null;
}
start() {
if (this.isRunning) return;
this.isRunning = true;
this.state = {
running: true,
n: 0,
startedAt: Date.now(),
stallMs: 320
};
console.log('Starting Promise microtask flood...');
enqueueMicrotaskFloodStep(this.state);
}
stop() {
if (this.state) this.state.running = false;
this.isRunning = false;
}
}
module.exports = new MicrotaskFloodDemo();
```

How to Capture Diagnostic Data for Troubleshooting Microtask Flood

yCrash is a diagnostic tool that captures runtime performance data from a live Node.js process, including CPU usage, event loop lag, worker thread activity, and call stacks, and turns that data into a report that points to the root cause. To set up yCrash for your Node.js application, follow the steps below:

1. Install yCrash

yCrash is available in both Cloud and On-premises versions. Use one of the options below to get started:

  • Cloud Service: Create an account to use the cloud version.
  • On-premises: Register for a 14-day trial and install the yCrash application on your local machine or within the organization’s environment.

2. Set up the yCrash Node.js hook first

Please note that this is a one-time setup. The hook must be loaded into your Node.js application before yCrash can capture the Node.js internals data that matters for diagnosis, things like worker thread CPU, event loop lag, method profiling, and call stacks. To set this up, follow the Hook Mode steps here: Node.js Diagnostic Capture. This hook setup adds very minimalistic, almost zero overhead to your application. 

3. Launch yc-360 script

Configure and start the yc-360 script to monitor your Node.js process. The full setup and configuration options specific to your deployment are available here: Micro-metrics Monitoring (M3) Mode

When the yc-360 script detects a problem, it captures  360° diagnostic artifacts (GC log, Event loop lags, Worker threads, CPU Profile, Unhandled rejections, Call stack, Active handles, Process, Storage, Kernel, Network, etc.). It uploads them to the yCrash server automatically. yCrash server analyzes the artifacts using advanced pattern recognition and ML algorithms to identify the root cause of the problem.

How to Analyze Microtask Flood Using the Diagnostic Data

In this section, we’ll see how to analyze a Microtask Flood using the diagnostic data captured by yCrash. We’ll start with the incident on the calendar dashboard, check the RCA Summary to understand what’s going on, and then drill into Event Loop Lag and the CPU profile to find the Promise drain.

Step 1: Open the incident from the yCrash dashboard

When the yc-360 script detects any performance degradation in the Node.js application, it automatically captures 360° diagnostic data and transmits it to the yCrash server. The yCrash server analyses this data and creates an incident in the yCrash dashboard as shown in the screenshot below. Open that incident from the yCrash calendar dashboard to view the reports.

Fig: yCrash dashboard, incident list view

Step 2: Start with the RCA Summary Page

Before diving into details, let’s start with the RCA Summary. yCrash analyzes the Node.js diagnostic data and brings the key findings together in simple language with their severity levels.

In the RCA Summary, we will focus on two sections:

  • AI Overview
  • Issues in the application and device

AI Overview

At the top of the RCA summary report page, you’ll see an AI Overview section. It gives you an AI-generated summary of the incident, which would make even a non-technical person understand the root cause of the problem. Within the AI Overview section, below the AI summary, you can also click on the ‘Dive Deeper in AI Mode‘ button to open a chat window to ask any question to the AI about this incident.

Fig: RCA Summary highlighting the AI Overview


As shown in the AI Overview above, the event loop stalled for 581 ms, and the hot method is `microtaskFloodStep` at 92.95% of CPU. The overview also mentions an event listener leak. That is a side note. Listeners going up do not drain the microtask queue.

Issues in application and device

Fig: RCA Summary highlighting the issues in application and device

As shown in the above screenshot, 4 application-level issues and 2 device-level issues are reported. The 2 that matter most for a Microtask Flood are:

  • Event loop blocked
  • Hot method

Skip the rest for this walkthrough. Listeners went from 140 to 175. The ms package is loaded at 2.0.0 and 2.1.3. Disk usage on C and D is a host note. None of that starved the Promise queue. App Memory on this capture is about 0% and App CPU is about 16%.

Step 3: Confirm the stall in Event Loop Lag

On the RCA Summary, click the “details here” hyperlink present at the end of the “Event loop blocked” problem. This will take you to the Event Loop Lag section in the Node.js internals report. 

Event Loop Lag tells how late the event loop ran by comparing when a timer was supposed to run with when it actually ran. High lag means the main thread was busy and could not process other events.

Fig: Event Loop Lag in Node.js Internals report

As you can see in the above graph, the first sample is a 581 ms Severe Spike at 19:04:44. Then the loop sits around 321–323 ms for the rest of the window. That plateau is the 320 ms drain. Each batch held timers and I/O for a few hundred milliseconds.

Event Loop Lag does not name the function. To prove this was Promise microtasks, we will review the CPU Profiler report next.

Step 4: Use CPU Profiling to close the investigation

Open the CPU Profile report from the left navigation as shown in the screenshot below. In the CPU Profiler Report page, yCrash aggregates the V8 sampling profile into a Function Explorer table (hot methods ranked by self CPU %). It provides an option to inspect the hot methods for deep diagnosis.

Open the CPU Profiler Report page from the left navigation and scroll to the Function Explorer section.

Fig: Function Explorer in CPU Profiler report

The Function Explorer confirms what we saw on the RCA Summary page. The `microtaskFloodStep` used 92.95% of CPU across 26,278 calls (94.14% total) on one thread. enqueueMicrotaskFloodStep` sits lower at 1.17%. (idle) is only 0.36%. The `runMicrotasks` shows up in the table too, with almost no self CPU; that is the Node runner, not the application work.

For deep diagnosis, let’s click on the “Inspect” button from the same row.

Fig: Inspector view of microtaskFloodStep

As you can see in the Inspect view above, `microtaskFloodStep` is the selected frame. Self CPU and Total CPU match the explorer: 92.95% and 94.14%, 26,278 calls. Expand the parents, and you land on `runMicrotasks`.

How to Fix Microtask Flood in Node.js

The following are the potential solutions for this issue:

  1. Bound how much work you put on Promise.then and queueMicrotask. Do not chain microtasks from inside a microtask with no stop condition.
  2. Move heavy or unbounded follow-up work to setTimeout, setImmediate, or a worker. Microtasks are for a small, must-finish-this-turn job.
  3. Use a named function for the callback so Method Profiling can show it. Anonymous arrows hide the cause as (anonymous).
  4. Watch Event Loop Lag together with the CPU profile. Lag alone is not enough. The hot method and the runMicrotasks path are what name a microtask flood.

Conclusion

Diagnosing and identifying the root cause of a Microtask Flood can be challenging when the host CPU looks only moderately busy. In this example, the diagnostic data was captured using the yc-360 script and analyzed using the yCrash server, which helped correlate the 581 ms stall with microtaskFloodStep.

The analysis showed that the application was stuck draining a chain of `Promise.then` callbacks. Event Loop Lag remained around 321–323 ms after the initial 581 ms spike, and Inspect showed `microtaskFloodStep` running under `runMicrotasks`.

The fix is to limit how much microtask work is done at once, or move some of the work to a later event loop phase. This prevents one continuous chain of microtasks from blocking the event loop for too long.

Mahesh Devda
Mahesh Devda
Articles: 9

Share your Thoughts!

Discover more from yCrash

Subscribe now to keep reading and get access to the full archive.

Continue reading