Node.js Diagnostics and Event Loop Blocking

A Node.js service runs all JavaScript on one thread. When that thread is busy — parsing a 50 MB JSON payload, running a catastrophic regular expression, hashing passwords synchronously — every request waits, health checks time out, and the orchestrator restarts a process that was not actually broken. Other slowdowns look similar but have different causes: the libuv thread pool can be saturated by file system, DNS, compression, or crypto work while the event loop is idle, and a memory leak causes growing garbage collection pauses before the process finally crashes.

Fixing the right problem starts with measuring which one you have. Node.js ships the tools to do it: event loop metrics, CPU profiles, diagnostic reports, and heap snapshots.

TL;DR

Quick Example

Export event loop health as metrics:

Capture evidence from a misbehaving process:

Open .cpuprofile and .heapsnapshot files in Chrome DevTools (Performance and Memory panels).

Core Concepts

Telling the Problems Apart

Event Loop Metrics

The libuv Thread Pool

Some operations run on a small thread pool — four threads by default: most file system calls, dns.lookup, zlib, and several crypto functions. When many run at once, the rest queue, and those operations slow down even though the event loop is free. UV_THREADPOOL_SIZE can be raised (set it before startup), but reducing the work — caching DNS, using asynchronous resolvers, streaming compression at a proxy — is often better.

Diagnostic Tools

Finding a Memory Leak

  1. Confirm growth: chart heap used and RSS over hours under normal traffic.
  2. Take a heap snapshot after warm-up, generate load, and take another (and a third).
  3. In DevTools, compare snapshots and sort by size delta; inspect the retainers of growing object types.
  4. Common culprits: module-level maps used as caches without eviction, event listeners added per request, timers never cleared, and closures capturing request objects.
  5. Fix, then verify with the same snapshot comparison.

Heap snapshots pause the process and can use as much memory again as the heap; take them on a replica removed from load balancing.

Fixing Event Loop Blocking

Best Practices

Instrument Before You Need It

Export event loop delay, ELU, heap usage, GC pauses, and active handles as metrics from day one, with alerts on sustained increases.

Make Diagnostics Available in Production

Enable --report-on-signal and reports on fatal errors, and document how to capture profiles and snapshots safely during incidents.

Separate Liveness From Busy

A blocked loop fails liveness probes and gets restarted, destroying the evidence. Capture a report or profile before restarts where possible, and avoid probe timeouts so short that brief stalls cause restarts.

Load Test With Realistic Payloads

Many blocking bugs only appear with production-sized payloads. Include large and pathological inputs in performance tests.

Common Mistakes

Synchronous Hashing in a Request Handler

Unbounded In-Memory Caches

Scaling Out a CPU-Bound Bug

FAQ

What does it mean to block the event loop?

JavaScript code runs for a long time without returning control, so Node.js cannot process other callbacks — new requests, timers, or I/O results — until it finishes. Every concurrent request is delayed.

How do I measure event loop lag in Node.js?

Use perf_hooks.monitorEventLoopDelay() for a histogram of delays and performance.eventLoopUtilization() for busy time, and export both as metrics.

How do I find a memory leak in Node.js?

Take heap snapshots at intervals under load, compare them in Chrome DevTools to find object types that keep growing, and follow their retainer paths to the code that holds them.

What is UV_THREADPOOL_SIZE?

The number of threads in libuv's pool, which handles file system operations, dns.lookup, compression, and some crypto. The default is 4; increasing it helps when those operations queue, but reducing the work is often better.

When should I use worker threads?

For CPU-intensive JavaScript — parsing, image or data processing, heavy computation — that would otherwise block the event loop. For I/O-bound work, the event loop is already efficient.

Related Topics

References