Node.js
async calls
performance
debugging
optimization

Node JS discovering slow async calls

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Finding slow asynchronous calls in Node.js is mostly an observability problem, not a syntax problem. You need to measure how long each async boundary takes, correlate that timing with request context, and separate slow external dependencies from event-loop blocking in your own code.

Start with Simple Timing

The fastest first step is to time suspected operations directly.

javascript
1async function loadUser(db, id) {
2  const start = performance.now();
3  const user = await db.query("select * from users where id = ?", [id]);
4  const ms = performance.now() - start;
5
6  console.log("db.query loadUser ms=", ms.toFixed(2));
7  return user;
8}

This is simple and effective for narrowing the search, especially when you already suspect one database call, HTTP request, or file-system operation.

Use perf_hooks for Better Instrumentation

Node’s perf_hooks module provides more structured timing than ad hoc Date.now() calls.

javascript
1const { performance } = require("node:perf_hooks");
2
3async function fetchProfile(fetcher, id) {
4  const start = performance.now();
5  const result = await fetcher(id);
6  const duration = performance.now() - start;
7
8  console.log(`fetchProfile took ${duration.toFixed(1)}ms`);
9  return result;
10}

This is a good pattern for app-level probes.

Distinguish Slow Async from Event-Loop Blocking

A slow async call and a blocked event loop are different issues.

  • slow async call means the app is waiting on an external operation or deferred work
  • event-loop blocking means your JavaScript is monopolizing the main thread

If requests feel slow, check event-loop delay too. Node exposes tools for that through monitorEventLoopDelay.

javascript
1const { monitorEventLoopDelay } = require("node:perf_hooks");
2
3const histogram = monitorEventLoopDelay();
4histogram.enable();
5
6setInterval(() => {
7  console.log("event loop p95 ms", histogram.percentile(95) / 1e6);
8  histogram.reset();
9}, 5000);

If event-loop delay is high, the problem may not be the async call itself.

Trace External Dependencies

Many slow async paths come from:

  • databases
  • HTTP APIs
  • Redis or message brokers
  • file or cloud storage

Instrument those boundaries explicitly. A wrapper around outbound operations is often enough:

javascript
1async function timed(label, fn) {
2  const start = performance.now();
3  try {
4    return await fn();
5  } finally {
6    const ms = performance.now() - start;
7    console.log(`${label} took ${ms.toFixed(1)}ms`);
8  }
9}

Then use it consistently:

javascript
const result = await timed("redis.get session", () => redis.get(sessionId));

This produces comparable measurements across the codebase.

Use Request Correlation

Timing data is much more useful when it is tied to one request or job. Without correlation, logs from concurrent requests become hard to interpret.

A common pattern is to attach a request ID and include it in every timing log. For larger systems, distributed tracing tools such as OpenTelemetry or vendor APM products are the right long-term answer.

Those tools let you see a whole async chain instead of isolated timings.

Async Hooks and Profilers

When the problem is not obvious, use deeper tooling:

  • 'async_hooks for tracking async resource lifecycles'
  • the Node inspector and CPU profiles for blocking work
  • APM products for distributed tracing and span timing
  • database slow-query logs for downstream confirmation

Do not jump straight to async_hooks unless simpler timing failed. It is powerful, but it is also more complex and noisier than targeted instrumentation.

Common Pitfalls

The biggest mistake is timing only the outer request and not the individual async dependencies inside it. That tells you the request is slow but not why.

Another issue is blaming async code when the real culprit is CPU-bound JavaScript blocking the event loop.

Teams also often log timings without correlation IDs, making the data hard to use under concurrency.

Finally, do not ignore the downstream systems themselves. A Node app may be healthy while the real bottleneck is the database, network, or third-party API.

Summary

  • Measure suspected async boundaries directly before reaching for heavy tooling.
  • Use perf_hooks for reliable duration measurements.
  • Check event-loop delay so you do not confuse blocking code with slow I/O.
  • Wrap external dependencies with consistent timing and request correlation.
  • Move to async_hooks, tracing, or APM only when basic instrumentation is not enough.

Course illustration
Course illustration

All Rights Reserved.