Diagnosing Event Loop Blocking in Node.js Applications
Spot event loop blocking, run trace‑sync‑io, measure delay with perf_hooks, and use clinic.js flamegraph to verify fixes that restore low latency.
31 Aug 2026, 22:37 UTC

Recognizable Condition
When a Node.js service shows rising response times, uneven request throughput, or HTTP handlers that appear stalled while CPU usage stays low, the event loop is likely being blocked by synchronous work.
Cause / Diagnostic Table
| Symptom | Likely Blocking Source |
|---|---|
| Steady increase in latency under constant load | Synchronous file system calls (fs.*Sync) or crypto operations |
| Latency spikes that correlate with request size | Large JSON.parse/stringify or heavy object serialization |
| Periodic stalls every few seconds despite idle CPU | Tight loops or blocking CPU‑bound work without yielding |
| High latency only when using worker_threads | Excessive data transfer between main thread and workers |
Ordered Checks
-
Enable synchronous I/O tracing
Run the application with the
--trace-sync-ioflag to log any synchronous file system or crypto calls.node --trace-sync-io /path/to/app.jsWhere to run: Development or staging environment; same user that runs the app.
Permissions: No special rights needed; just ability to start the process.
Expected check: Look for warnings like
[Sync I/O] fs.readFileSyncin stderr. Absence of such warnings suggests the blocking source is not sync I/O.Risk: Minimal; the flag adds a small overhead but does not change behavior.
-
Measure event loop delay with a sampler
Use the built‑in
perf_hooksmodule to record the delay between loop ticks.const { performance, PerformanceObserver } = require('perf_hooks'); const obs = new PerformanceObserver((items) => { const entry = items.getEntries()[0]; console.log(`eventLoopDelay: ${entry.duration} ms`); }); obs.observe({ entryTypes: ['measure'], buffered: true }); setInterval(() => { performance.mark('A'); // allow any pending callbacks to run performance.mark('B'); performance.measure('eventLoopDelay', 'A', 'B'); }, 1000);Where to run: Insert the snippet near the start of your main file; run the app normally.
Permissions: Same as the app.
Expected check: Under load, the printed delay should stay below a chosen threshold (e.g., 10 ms). Repeated values above that indicate blocking.
Risk: Adds negligible overhead; ensure the observer is detached in production if not needed.
-
Profile with clinic.js flamegraph
Use
clinic doctorcombined with a load generator such asautocannonto visualize where time is spent.clinic doctor --on-port='autocannon localhost:3000' -- /path/to/app.jsWhere to run: A machine that can generate traffic; same Node version as production.
Permissions: Ability to install clinic and autocannon globally or via npx.
Expected check: In the flamegraph, look for a wide yellow block representing the event loop; a narrow block (< 5 ms) is healthy. A wide red or yellow area indicates blocking work.
Risk: The profiling adds overhead; limit the test duration to a few minutes.
Fixes Tied to Findings
- Replace synchronous file system calls – change
fs.readFileSync(path)toawait fs.promises.readFile(path)(or use callbacks). - Offload heavy JSON work – for payloads > 100 KB, use
JSON.parse/stringifyinside aworker_threadsthread or split the object and process incrementally. - Avoid tight loops without yielding – insert
await setImmediate(() => {})or break the loop into chunks processed viasetTimeout. - Move CPU‑intensive crypto or compression – use the asynchronous APIs (
crypto.pbkdf2,zlib.promise) or delegate to a worker thread. - Check worker‑thread data transfer – if you already use workers, measure the size of posted messages; large buffers should be transferred via
Transferableobjects (arrayBuffer.transfer) to avoid copying.
Escalation Criteria
- If after converting all identified synchronous calls to async versions the event loop delay remains above the threshold, proceed to CPU profiling (
--inspectorclinic cpu-profiler) to uncover hidden synchronous work or inefficient algorithms. - If introducing worker threads does not improve throughput and instead adds latency, verify that the work performed off‑thread truly outweighs the message‑passing cost; consider a process‑level scaling approach (multiple Node instances) or a redesign that reduces the computational load per request.
- When latency correlates with garbage collection pauses, enable
--trace-gcto distinguish GC stalls from event loop blocking; if GC is the culprit, tune heap size or consider a different allocation strategy.
Verification
After applying a fix, repeat the checks:
- Run with
--trace-sync-ioand confirm no new sync I/O warnings appear. - Observe the
perf_hookssampler; the average delay should stay under the target (e.g., 10 ms) under the same load that previously caused spikes. - Run
clinic doctoragain; the flamegraph should show a narrow event loop band and the previously wide blocking segment reduced or removed.
If all three indicators improve, the blocking issue is mitigated.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.