How to Troubleshoot Synchronous Blocking 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.

Synchronous Block is one of those production issues that looks quiet on an average CPU chart. The process is still up, memory looks fine, and then requests suddenly wait for a second or more because the main thread is stuck inside sync JSON work, sync crypto, or another blocking call.

In this blog, we’ll walk through how Synchronous Block 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 ‘Synchronous Block’ in Node.js?

Node.js runs your application on a single main thread. That thread also runs the event loop, so timers, I/O callbacks, and HTTP responses all depend on it. A synchronous block means that thread stays inside JavaScript or a sync native call for a long stretch without returning to the loop.

While that happens, new work has to wait. Event Loop Lag is how you measure the delay: the gap between when a timer should have fired and when it actually ran. High lag is the symptom. The CPU Profiler is what names the function that burned the time.

What causes ‘Synchronous Block’ in Node.js?

Here is the usual list of causes for the ‘Synchronous Block’ problem.

  1. Large synchronous JSON work: JSON.stringify or JSON.parse on big objects on a request path or timer callback can stall the main thread for a long time.
  2. Synchronous crypto: Calls like pbkdf2Sync or scryptSync on the main thread finish only when the whole derivation is done.
  3. Sync file and compression APIs: readFileSync, gzipSync, and similar blocking fs/zlib calls keep the event loop waiting until I/O and CPU work finish.
  4. Expensive regex or parsing: A slow regular expression or a hand-rolled parser that runs to completion can delay every other callback.
  5. Accidental busy loops: A for/while loop that does real work without yielding, setImmediate, or a worker offload.

How to Simulate a Synchronous Block in Node.js

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

The following program builds a large object, runs JSON.stringify/parse, then keeps calling `pbkdf2Sync` until about 1.2 seconds have passed on the main thread. After a short pause, it does the same thing again.

``` JS Code (sync-block.js)
const crypto = require('crypto');
function runSyncBlockTick() {
const largeObj = {};
for (let i = 0; i < 1000; i++) {
largeObj[`key_${i}`] = {
data: 'x'.repeat(500),
nested: { a: 1, b: [1, 2, 3] }
};
}
JSON.parse(JSON.stringify(largeObj));
for (let i = 0; i < 8; i++) {
crypto.pbkdf2Sync('password', 'salt', 10000, 64, 'sha512');
}
}
function runSyncBlockTickUntil(targetMs) {
const end = Date.now() + targetMs;
do {
runSyncBlockTick();
} while (Date.now() < end);
}
class SyncBlockDemo {
constructor() {
this.isRunning = false;
this.timeoutId = null;
this.blockCount = 0;
}
_scheduleMainTick() {
if (!this.isRunning) return;
const started = Date.now();
runSyncBlockTickUntil(1200);
console.log(`Sync block: ~${Date.now() - started}ms main-thread stall`);
this.blockCount++;
this.timeoutId = setTimeout(() => this._scheduleMainTick(), 400);
}
start() {
if (this.isRunning) return;
this.isRunning = true;
console.log('Starting synchronous block simulation...');
this._scheduleMainTick();
}
stop() {
clearTimeout(this.timeoutId);
this.isRunning = false;
this.blockCount = 0;
}
}
module.exports = new SyncBlockDemo();
```

How to Capture Diagnostic Data for Troubleshooting Synchronous Block

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 Synchronous Block Using the Diagnostic Data

In this section, we’ll see how to analyze a Synchronous Block 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 reports to find the code that is blocking the main thread.

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 root cause is a blocked event loop that stalled for over 1.2 seconds.

The overview also mentions an event listener leak warning. That warning is secondary here; the stall is the problem we care about.

Issues in application and device

Fig: RCA Summary highlighting the issues in application and device


As shown in the above screenshot, several application-level issues are reported. The 2 that matter most for a Synchronous Block are:

  • Event loop blocked
  • Hot method

The event listener leak and duplicate ms package warnings show up in this capture too, but they do not explain the freeze. You can set them aside for this investigation.

Step 3: Confirm the stall in Event Loop Lag

Open the Node.js Internals report from the left navigation as shown in the screenshot below. In the top section of this report called “Observations”, you will find all the problems detected in this report. Now let’s click the “detail here” hyperlink present at the end of the Event Loop blocked problem statement. This will take you to the Event Loop Lag section.

Fig: Event Loop Lag in Node.js Internals report


As you can see in the above graph, the lag stays near about 1250 ms for most of the window, with Fatal Spike rows such as 1247 ms, 1258 ms, and 1257 ms. That lines up with the ~1.2-second synchronous tick in the sample app.

Event Loop Lag does not name the function. To find out which code burned that time, we will review the CPU Profiling report next.

Step 4: Use CPU Profiling to close the investigation

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 to do the 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: `run` used 69.98% of CPU across 19,281 calls. You will also see `runSyncBlockTick` and `runSyncBlockTickUntil` with high Total CPU underneath that path.

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

Fig: Inspector view of the hot method

As you can see in the Inspect view above, run is the native frame behind Node’s crypto work. Expand it, and you will see most of that time in `pbkdf2Sync`. Expand again and the parent chain points back to `runSyncBlockTick` and `runSyncBlockTickUntil` in `sync-block.js`.

This is useful because the CPU Profiler doesn’t just tell you that the event loop was blocked. It shows that the event loop was blocked by synchronous JSON and crypto work on the main thread, not by waiting on I/O.

How to Fix a Synchronous Block in Node.js

The following are the potential solutions for this issue:

  1. Replace pbkdf2Sync and other sync crypto with async APIs, or move them to a bounded worker pool.
  2. Avoid giant JSON.stringify/parse on the request path; stream, paginate, or move heavy transforms off the main thread.
  3. Break long tasks into smaller chunks that yield with setImmediate() or setTimeout(0).
  4. Watch Event Loop Lag in production so sync regressions show up early.

Conclusion

Diagnosing and identifying the root cause of Synchronous Block can be challenging, especially when average CPU looks low, and the process still appears healthy. In this example, the diagnostic data was captured using the yc-360 script and analyzed using the yCrash server, which helped correlate Event Loop Lag with the hot sync path on the CPU profile.

The analysis traced the issue to synchronous JSON work and pbkdf2Sync calls inside runSyncBlockTick / runSyncBlockTickUntil in sync-block.js. By examining the RCA Summary, the Event Loop Lag chart, and the CPU Profiler Inspect view, we could connect the Fatal 1286 ms stall to that code path.

Moving sync crypto and large JSON work off the main thread, or breaking it into smaller yielding chunks, addresses the underlying problem and helps prevent the issue from recurring.

Mahesh Devda
Mahesh Devda
Articles: 14

Share your Thoughts!

Discover more from yCrash

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

Continue reading