Course outline

Node.js / Advanced: reliable Node.js services / Lesson 14

Event-loop delay and profiling

Distinguish waiting from CPU pressure and collect evidence before changing a running system’s architecture.

About 4 min readContent reviewed
On this page

Lesson goals

  • Explain the concepts and trade-offs behind event-loop delay and profiling.
  • Predict and verify both examples, including their outputs and cleanup behavior.
  • Diagnose the three common failures and apply the worked solutions to related inputs.

Start with an observable symptom

A slow request does not identify its cause. Time can be spent waiting for a connection, reading an upstream response, executing JavaScript, collecting garbage, or sitting in an application queue. Record these stages separately and compare a normal period with an incident. High CPU and elevated event-loop delay suggest a different investigation from low CPU and a saturated database pool. Avoid treating one host-wide utilization number as proof about a specific Node process. Establish the workload, runtime, deployment limits, and time window before drawing conclusions.

Measure delay without promising exact timing

monitorEventLoopDelay samples how late scheduled loop work runs. Its histogram values use nanoseconds, so convert deliberately before displaying milliseconds. The first example enables a short observation window and checks that samples exist instead of publishing a fabricated universal latency. Resolution changes overhead and what the histogram can observe. A short idle sample cannot establish production tail latency, and the monitoring tool itself is not a load test. In a service, reset or export interval data intentionally and separate shutdown artifacts from ordinary measurement windows.

Read utilization as a ratio

eventLoopUtilization reports active and idle loop time and a utilization ratio. It is not identical to operating-system CPU usage: synchronous blocking in certain calls can keep the loop active while CPU behavior differs. The second example captures a baseline and computes a delta after a timer wait. Only the type and valid range are asserted because actual ratios depend on scheduling and workload. Compare ratios across consistent windows and correlate them with delay, request throughput, and host metrics before claiming that the process needs more cores.

javascript Complete example: Read utilization as a ratio
import { monitorEventLoopDelay } from 'node:perf_hooks';
import { setTimeout as delay } from 'node:timers/promises';
const histogram = monitorEventLoopDelay({ resolution: 10 });
histogram.enable();
try {
  await delay(100);
  console.log(histogram.count > 0);
  console.log(Number.isFinite(histogram.mean / 1e6));
} finally { histogram.disable(); }
Expected output
true
true

Samples are collected over a local idle window. The conversion divides nanoseconds by one million; no fixed latency is asserted.

Capture a focused profile

A CPU profile samples executing stacks and helps locate functions consuming CPU during a representative window. Use the official inspector or CPU profiling options in a controlled environment, protect diagnostic artifacts, and avoid exposing an inspector listener publicly. Heap snapshots answer a different question: what objects are retained. They can pause the process and require substantial memory, so choose the capture environment and safety margin carefully. Start with a narrow hypothesis and one artifact. A large collection of unrelated traces can increase operational risk without making the cause clearer.

javascript Complete example: Capture a focused profile
import { performance } from 'node:perf_hooks';
import { setTimeout as delay } from 'node:timers/promises';
const start = performance.eventLoopUtilization();
await delay(20);
const delta = performance.eventLoopUtilization(start);
console.log(typeof delta.utilization);
console.log(delta.utilization >= 0 && delta.utilization <= 1);
Expected output
number
true

The delta describes this observation interval. Its numeric value is intentionally not presented as a portable expected benchmark.

Turn evidence into a verified change

If a profile points to repeated JSON serialization, reduce repeated work or cache an appropriate representation before introducing threads. If allocation drives garbage collection, examine retention and object lifetime rather than assuming a leak. A leak means unwanted retention over time, which requires comparable snapshots or metrics under comparable load. Re-run the same representative workload after one change and compare correctness, throughput, tail latency, and memory. Keep rollback possible. Profiling is a way to narrow uncertainty, and an optimization claim is only as strong as the experiment that supports it.

Common errors and fixes

Delay is a million times too large

Why it happens
Nanoseconds were labeled as milliseconds.
How to fix it
Divide by 1e6 and label units.
Prevent it next time
Keep units in metric names and dashboards.

Low CPU is mistaken for a healthy service

Why it happens
Requests are queued or blocked on an external dependency.
How to fix it
Measure pool wait and upstream timing separately.
Prevent it next time
Correlate resource and request metrics.

A diagnostic capture causes an outage

Why it happens
A memory-heavy artifact was captured without headroom.
How to fix it
Use an isolated replica or controlled reproduction.
Prevent it next time
Plan capture overhead and protect artifacts.

Check your understanding

1. A histogram percentile is 25000000 nanoseconds. Convert it and explain its limitation.

Show worked solution

Divide by 1000000 to obtain 25 ms. It describes sampled event-loop delay for that observation window, not total HTTP response time and not every future workload. Compare equivalent windows and correlate with request latency before assigning cause.

2. Requests slow down while CPU stays low and pool wait grows. What should you investigate first?

Show worked solution

Inspect connection usage, unreleased connections, query duration, and dependency capacity. More JavaScript worker threads do not fix a saturated database pool. Verify the suspected cause with pool metrics and a controlled reproduction before changing concurrency or database limits.

Key takeaways

  • A slow request does not identify its cause.
  • monitorEventLoopDelay samples how late scheduled loop work runs.
  • If a profile points to repeated JSON serialization, reduce repeated work or cache an appropriate representation before introducing threads.

Official references