Overview

11 Diagnostics

Diagnostics in JavaScript is less about collecting data than understanding what the data means. The chapter frames runtime signals as practical clues from the engine, runtime, and operating system, then builds a shared vocabulary for interpreting them across Node.js, Deno, Bun, browsers, and constrained environments. It emphasizes that the same symptom can have very different causes, so the key skill is learning to read memory, CPU, scheduling, and I/O signals together rather than in isolation.

The memory section shows how to tell a leak from normal garbage-collection pressure by watching trends in heap usage, GC traces, and heap snapshots. A true leak shows steadily rising troughs, declining collector effectiveness, and objects that survive across snapshots because some reference keeps them alive. The chapter explains how to use snapshot comparisons, retainer chains, and allocation profiling to identify what is accumulating and why, while also warning that high allocation volume or long-lived objects can create pressure without a leak.

For performance, the chapter distinguishes genuine CPU bottlenecks from event-loop delay, thread-pool saturation, and I/O latency. It introduces CPU profiles, deoptimization tracing, event-loop delay monitoring, and event-loop utilization as complementary tools for locating hot functions, spotting repeated optimize-bailout cycles, and deciding when to scale. The closing message is that tools are runtime-specific, but the underlying mental model is portable: if you understand garbage collection, scheduling, object shapes, and execution flow, you can diagnose problems even when a particular diagnostic feature is missing.

A healthy memory graph shows regular, narrow spikes with consistently level troughs; while memory leaks gradually increase over time.
The Chrome/Edge DevTools Performance Tab
The Chrome/Edge DevTools connected to a Node.js instance
A heap-snapshot from Node.js in the Chrome/Edge DevTools Memory tab.
Collecting an Allocation sample in DevTools
An allocation sample generates what is essentially an allocation flamegraph representing a breakdown of where in your application and the runtime the most memory is being allocated. This is essential to determining precisely where excess memory is being allocated. The wider the stack.
The Chrome/Edge DevTools Performance Tab provides detailed CPU and wall-time profiling of where your application is spending time.
SessionRecord and its per-record payload Array climb together and nothing else moves.
The partial output from V8’s --prof-process command showing a breakdown of where the application was spending the most time while the profile was being collected. In the past, I would have described step-by-step how to interpret these files but now, uploading one of these to an AI agent for analysis often reveals better analysis and insight, especially if the agent also has access to your code.
The performance profile details panel breaks down self time vs total time for each sampled function.
The call tree illustrates the actual way that functions were called.
Each horizontal bar represents a function. Width represents time the function was on the stack.

Summary

  • process.memoryUsage() and process.resourceUsage() provide two complementary views of your application: the JavaScript engine's view of memory and the operating system's view of the process. The relationship between rss, heapTotal, heapUsed, and external narrows which category of memory is growing before you reach for heavier tools.
  • A memory leak is a failure to make objects unreachable, not a failure to free memory. The diagnostic flow is: confirm the symptom with process.memoryUsage() over time, distinguish leak from pressure with --trace-gc, find what is accumulating through heap snapshot comparison, and find why it is retained through retainer chains.
  • GC pressure is distinct from a leak. The collector is reclaiming correctly but working too hard. The --trace-gc signals are increasing Scavenge frequency, Mark-Compact events appearing under load, and mu declining. The cause is either high allocation volume or objects surviving into old generation longer than necessary.
  • CPU profiles are statistical samples that show where the engine spent time, not where your source code spent time. --prof, --cpu-prof, and DevTools Performance panel each show a different layer of the same execution. The same code produces fundamentally different profiles on V8 and JSC because the engine implementations behind every builtin API are different.
  • Event loop delay measures how long tasks wait before the event loop processes them. It is distinct from how long tasks take to execute. The microtask queue drains within a macrotask turn and is not captured by monitorEventLoopDelay(), creating a blind spot for promise-heavy workloads.
  • Event loop utilization measures the ratio of time the event loop spends working versus waiting for I/O. It strips away background thread activity that inflates CPU utilization, making it a more reliable scaling signal for Node.js workloads than CPU percentage.
  • Diagnostic tools are runtime-specific. The full toolkit is available on Node.js with V8. On Bun with JSC, CPU profiling and heap snapshots are available but GC tracing, deoptimization tracing, and mu are not. On Cloudflare Workers, nearly nothing is accessible. The engine knowledge from the previous chapters is the only diagnostic instrument that works on every runtime.

FAQ

What is a diagnostic signal in JavaScript runtimes?A diagnostic signal is any piece of information from the engine, runtime, or operating system that helps determine what went wrong. JavaScript itself provides no diagnostics; the available signals come from the runtime and platform.
What does process.memoryUsage() tell you?It reports broad memory allocation data for the process: rss, heapTotal, heapUsed, external, and arrayBuffers. These values help distinguish JavaScript heap growth from native or external memory growth.
How can you tell the difference between a memory leak and garbage collection pressure?A memory leak usually shows steadily rising heapUsed with troughs that drift upward over time. Garbage collection pressure tends to produce a saw-tooth pattern with sharp spikes and irregular drops, often with declining mu in GC traces if pressure is severe.
When should you use heap snapshots instead of allocation timelines?Use heap snapshots when you want to see what objects survived and what is retaining them. Use allocation timelines when you need to study transient allocations and identify code that creates many short-lived objects between snapshots.
What do shallow size, retained size, and retainer chain mean in a heap snapshot?Shallow size is the memory used by the object itself. Retained size is the total memory that would be freed if that object were removed. The retainer chain is the path of references from a GC root to the object, showing what is keeping it alive.
What information does --trace-gc provide?It logs each garbage collection event in real time, including collection type, heap size before and after, pause duration, incremental marking details, mu (mutator utilization), and the reason the collection was triggered.
How do you identify a CPU bottleneck with profiling tools?Capture a CPU profile with --cpu-prof or DevTools, then inspect the bottom-up view for functions with the highest self-time. If a function is hot but also shows repeated deoptimization, the cost may come from unstable optimization rather than the function’s logic itself.
Why is event loop delay important, and how is it measured?Event loop delay measures how long tasks wait before the event loop can run them. In Node.js it is commonly measured with monitorEventLoopDelay(), and high p99 delay usually indicates blocking work, GC pauses, or too much synchronous execution on the main thread.
What is event loop utilization and why is it better than CPU utilization for scaling Node.js apps?Event loop utilization, or ELU, measures how much time the event loop is active versus waiting in the event provider. It is often better than CPU utilization because CPU can be inflated by background threads while the event loop still has capacity, or low while the event loop is completely saturated.
How do you diagnose thread pool saturation in Node.js?Look for unexpectedly slow operations like DNS lookup or filesystem calls, especially if they correlate with heavy crypto or I/O activity. You can inspect performance.nodeTiming.uvMetricsInfo, time thread-pool-backed operations directly, and remember that increasing UV_THREADPOOL_SIZE should only be done after confirming saturation.

pro $24.99 per month

  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose one free eBook per month to keep
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime

lite $19.99 per month

  • access to all Manning books, including MEAPs!

team

5, 10 or 20 seats+ for your team - learn more


choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • JavaScript in Depth ebook for free
choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • JavaScript in Depth ebook for free