Node.js Production Diagnostics & Profiling
Parent: JavaScript and Node.js · researched 2026-06-02T17:33:52.709Z· 7 sources · 8 concepts · skill nodejs-diagnostics-profiling
This reference is about measuring a real Node.js process — locating where CPU
Overview
- This reference is about measuring a real Node.js process - locating where CPU [source]
- time is spent, where memory is retained, and how to instrument code paths with [source]
- near-zero overhead in production. It is the "find the bottleneck" companion to two [source]
- sibling references that own the explanatory and tuning layers: [source]
- v8-engine-internals - owns GC mechanics and heap tuning (--max-old-space-size, [source]
- semi-space sizing, deopt reasons). This file uses heap snapshots to find leaks; [source]
- it does not re-explain generational GC. [source]
- nodejs-concurrency-internals - owns the libuv event-loop phase model, [source]
- microtask ordering, and stream backpressure. This file measures event-loop lag [source]
- (monitorEventLoopDelay, eventLoopUtilization); it does not re-derive the phases. [source]
- javascript-node-html-css-debugging-expert - owns interactive breakpoint/step [source]
- debugging and browser HTML/CSS DevTools. This file is about profiling, not stepping. [source]
- The mental model: Doctor → Flame/CPU profile → Heap snapshot. First classify the [source]
- symptom (CPU-bound, I/O-bound, GC-bound, event-loop-blocked), then reach for the tool [source]
- that resolves that class. Guessing without a profile is the cardinal anti-pattern. [source]
1. The Inspector / Chrome DevTools Protocol (CDP)
- Node embeds the V8 Inspector, which speaks the **Chrome DevTools Protocol over a [source]
- WebSocket**. Every richer tool (Chrome DevTools, VS Code, clinic, programmatic [source]
- profiling) is a CDP client underneath. [source]
- --inspect[=[host:]port] activates the inspector (default 127.0.0.1:9229; port 0 [source]
- = random). --inspect-brk breaks at the first line of the user script; --inspect-wait [source]
- (v22.2+) blocks until a client attaches. --inspect-port + SIGUSR1 lets you attach [source]
- to an already-running process. [source]
- Security: the inspector is a full code-execution channel. Never bind it to a public [source]
- interface (0.0.0.0) without a firewall - --inspect on a public IP is remote code [source]
- execution. Default to localhost and tunnel over SSH. [source]
- The WebSocket URL is discoverable at http://host:port/json/list or via [source]
- inspector.url(). --inspect-publish-uid controls where it's published. [source]
- Open chrome://inspect (or edge://inspect) → the target appears under "Remote Target" [source]
- → "inspect" gives a full DevTools UI (Performance + Memory tabs) wired to the Node process. [source]
2. Programmatic profiling via `node:inspector`
- You don't need an external client - drive the protocol in-process for self-profiling [source]
- (e.g., profile only a hot window, or on a signal). The promises API is cleanest: [source]
- **Profiler. domain → CPU profiles (.cpuprofile). HeapProfiler.** domain → [source]
- heap snapshots (.heapsnapshot) and allocation sampling; snapshot chunks arrive via the [source]
- HeapProfiler.addHeapSnapshotChunk event. Do not pass reportProgress: true to [source]
- HeapProfiler.takeHeapSnapshot. [source]
- inspector.open(port, host, wait) activates the inspector at runtime and (v20.6+) [source]
- returns a Disposable so using auto-closes it. inspector.waitForDebugger() blocks [source]
- until a client sends Runtime.runIfWaitingForDebugger. [source]
- In worker threads use session.connectToMainThread(); setting breakpoints on a [source]
- same-thread session is unsupported. [source]
3. The V8 profilers as CLI flags (no client needed)
4. Heap snapshots & the memory-leak hunting workflow
- A heap snapshot is a full graph of live objects and their retainers. Capture one: [source]
- On signal (production): --heapsnapshot-signal=SIGUSR2, then kill -USR2 <pid>. [source]
- Near OOM (catch the growth): --heapsnapshot-near-heap-limit=<count> (stable v25.4) [source]
- writes a snapshot as the heap approaches the limit - pair with --max-old-space-size. [source]
- Programmatically: v8.writeHeapSnapshot() or the HeapProfiler domain above. [source]
- The three-snapshot technique (the standard leak workflow in DevTools' Memory tab): [source]
- Snapshot at steady state (baseline). [source]
- Drive the suspected-leaking operation N times; force a GC; snapshot again. [source]
- Repeat; snapshot a third time. Use "Comparison" view between snapshots and the [source]
- "Objects allocated between snapshot 1 and 2" filter: anything still retained after [source]
- step 3 that grows linearly with N is the leak. Inspect the Retainers pane to find [source]
- what holds it - the usual culprits are module-scope Map/array caches without [source]
- eviction, event listeners never removed (emitter.on in a hot path), closures [source]
- capturing large scope, and timers holding references. [source]
5. `perf_hooks` — measurement, not sampling
- Where profilers sample stacks, perf_hooks gives precise, programmatic numbers: [source]
- PerformanceObserver with entryTypes (mark, measure, function, gc, http, [source]
- dns, net, …) and buffered: true to catch entries created before observe(). [source]
- performance.mark() / measure() / timerify(fn) - timerify wraps a function so [source]
- each call emits a function timeline entry (works with async, reports on settlement). [source]
- monitorEventLoopDelay({ resolution }) → an IntervalHistogram sampling event-loop [source]
- delay in ns: enable()/disable(), percentile(p), mean, max, stddev, reset(). [source]
- This is the right signal for "is the event loop lagging?" - a p99 in the tens of ms [source]
- means something is blocking. [source]
- performance.eventLoopUtilization() → { idle, active, utilization }. Take two [source]
- snapshots and diff (eventLoopUtilization(prev)); utilization near 1.0 means the loop is [source]
- saturated (CPU-bound), near 0 means it's mostly waiting (I/O-bound). It is the canonical [source]
- signal for worker-pool / thread-pool sizing decisions. [source]
- createHistogram() → a RecordableHistogram (record, recordDelta, percentile) [source]
- for your own latency distributions. [source]
6. `diagnostics_channel` — production instrumentation with zero idle cost
- The publish/subscribe channel built into Node for library + production instrumentation. [source]
- Its defining property: channel.hasSubscribers is false until something subscribes, so [source]
- guarded publishing costs almost nothing when no APM is attached. [source]
- TracingChannel (dc.tracingChannel(name)) emits a coordinated set of sub-channels [source]
- — tracing:<name>:start | end | asyncStart | asyncEnd | error - and wraps a unit of work [source]
- with traceSync, tracePromise, or traceCallback. Subscribe to all events at once [source]
- with tc.subscribe({ start, end, asyncStart, asyncEnd, error }). This is how APM vendors [source]
- trace async operations without monkey-patching. [source]
- Built-in channels ship for http(.server/.client)., http2., net.*, module [source]
- (require/import tracing), child_process, worker_threads, and console - subscribe to [source]
- get framework-level telemetry for free. channel.bindStore() integrates AsyncLocalStorage [source]
- for request-context propagation. [source]
7. Diagnostic reports & trace events
- Diagnostic report (--report-on-fatalerror, --report-on-signal, [source]
- --report-uncaught-exception, --report-signal=SIGUSR2, process.report.writeReport()): [source]
- a single JSON document with the JS + native stack, heap stats, libuv handles, resource [source]
- usage, and environment - the first artifact to grab on a crash or hang in production. [source]
- --trace-event-categories='v8,node,node.async_hooks' emits Chrome trace_events [source]
- (trace_*.log) loadable in chrome://tracing / Perfetto for a timeline across [source]
- subsystems. Heavier than the above; use for deep timeline correlation. [source]
Tools & frameworks
- The canonical combo - profile under load: 0x sets a $PORT to the first port the [source]
- profiled process opens and forwards a signal when the load test ends, so [source]
- 0x -P 'autocannon localhost:$PORT' server.js runs the load test, then auto-generates the [source]
- flame graph from exactly that window. (clinic does the same with clinic flame --autocannon.) [source]
Methodology — a triage workflow
- Reproduce under load. A profile of an idle process is noise. Drive realistic traffic [source]
- with autocannon (or your load tool). [source]
- Classify with monitorEventLoopDelay + eventLoopUtilization (or clinic doctor): [source]
- high ELU + high event-loop delay → CPU-bound / blocking; low ELU + high latency → [source]
- I/O-bound / async; sawtooth memory + GC pauses → GC/leak. [source]
- CPU-bound → --cpu-prof or a flame graph (0x / clinic flame). Read top-down for the [source]
- hot path; look for an unexpectedly wide synchronous frame (sync crypto, JSON of a huge [source]
- payload, a regex → ReDoS). [source]
- I/O-bound → clinic bubbleprof or diagnostics_channel HTTP/net channels; look for [source]
- serialized awaits that should be Promise.all, missing connection pooling, or a chatty [source]
- Memory growth → heap snapshot three-snapshot diff; find the retainer. [source]
- Instrument the winner with diagnostics_channel / perf_hooks so the metric is [source]
- permanent and you can alert on regressions - don't re-profile by hand each time. [source]
Practical patterns
- Profile a window, not the whole run - use the programmatic Profiler.start/stop [source]
- around the suspect path to keep the profile small and readable. [source]
- On-demand production capture - ship with --heapsnapshot-signal=SIGUSR2 and a [source]
- process.on('SIGUSR2') CPU-profile toggle so you can capture artifacts from a live pod [source]
- without a redeploy. Pull the files and open them in local DevTools. [source]
- Guard every publish with hasSubscribers so instrumentation is free when no [source]
- collector is attached. [source]
- Alert on eventLoopUtilization and event-loop delay p99, not just CPU% - they catch [source]
- blocking that CPU% averages hide. [source]
Anti-patterns
- Optimizing without a profile. "I think this loop is slow" → measure first; the hot [source]
- path is almost never where intuition points. [source]
- Profiling an idle / unrealistic process - no load, or synthetic data that doesn't [source]
- exercise the real path. [source]
- console.time everywhere as a profiler - fine for one span, useless for finding an [source]
- unknown bottleneck; it can't see native frames or aggregate. [source]
- Leaving --inspect bound to a public interface - remote code execution. [source]
- Unguarded channel.publish(expensiveToBuild()) - defeats the zero-idle-cost design. [source]
- **Treating a heap snapshot as GC *tuning*** - the snapshot finds the leak; sizing the [source]
- heap (--max-old-space-size) is a v8-engine-internals concern. [source]
Troubleshooting
- Can't connect Chrome DevTools → check the WS URL (inspector.url() / /json/list), [source]
- confirm the port isn't firewalled, and that you used --inspect not --inspect-brk [source]
- (which pauses before your code). [source]
- .cpuprofile is empty / tiny → the profiler window didn't overlap the workload; [source]
- start it before driving load, stop it after. [source]
- Heap snapshot too big to open → raise DevTools memory or use allocation sampling [source]
- (--heap-prof) instead of a full snapshot for a first pass. [source]
- No gc entries from PerformanceObserver → GC timeline entries require observing [source]
- entryTypes: ['gc']; for GC tuning and pause analysis, see v8-engine-internals. [source]
- High event-loop delay but flat CPU → blocking is in the libuv thread pool [source]
- (fs/dns/crypto/zlib) saturating UV_THREADPOOL_SIZE; see nodejs-concurrency-internals. [source]
References
- Node.js - Inspector module (node:inspector, CDP, Session, Profiler/HeapProfiler): https://nodejs.org/api/inspector.html [source]
- Node.js - CLI diagnostic flags (--inspect, --cpu-prof, --heap-prof, --prof, --heapsnapshot-signal, --report-*, --trace-event-categories): https://nodejs.org/api/cli.html [source]
- Node.js - perf_hooks (PerformanceObserver, monitorEventLoopDelay, eventLoopUtilization, timerify, createHistogram): https://nodejs.org/api/perf_hooks.html [source]
- Node.js - diagnostics_channel (channels, TracingChannel, built-in channels, bindStore): https://nodejs.org/api/diagnostics_channel.html [source]
- clinic.js - node-clinic (doctor / flame / bubbleprof): https://github.com/clinicjs/node-clinic ; NearForm "Introducing Clinic.js": https://nearform.com/insights/introducing-node-clinic-a-performance-toolkit-for-node-js-developers/ [source]
- 0x - single-command flame graphs: https://github.com/davidmarkclements/0x ; NearForm "Tuning Node.js app performance with Autocannon and 0x": https://nearform.com/insights/tuning-node-js-app-performance-with-autocannon-and-0x/ [source]
- autocannon - HTTP load generator: https://github.com/mcollina/autocannon [source]
Children
- Inspector / Chrome DevTools Protocol (--inspect/--inspect-brk/--inspect-wait, inspector.Session, CDP over WebSocket) (frontier)
- Programmatic profiling via node:inspector (Profiler.* / HeapProfiler.* domains) (frontier)
- V8 profilers as CLI flags (--prof/--prof-process tick processor, --cpu-prof, --heap-prof) (frontier)
- Heap snapshots & three-snapshot memory-leak hunting (--heapsnapshot-signal, --heapsnapshot-near-heap-limit, retainer analysis) (frontier)
- perf_hooks measurement (PerformanceObserver, monitorEventLoopDelay histogram, eventLoopUtilization for pool sizing, timerify, createHistogram) (frontier)
- diagnostics_channel + TracingChannel (zero-idle-cost instrumentation, built-in http/net channels, bindStore) (frontier)
- Diagnostic reports (--report-*) & trace_events (--trace-event-categories) (frontier)
- Ecosystem tools: clinic.js (doctor/flame/bubbleprof), 0x flamegraphs, autocannon load-driven profiling (frontier)
Frontier under this node: Diagnostic reports (--report-*) & trace_events (--trace-event-categories), Ecosystem tools: clinic.js (doctor/flame/bubbleprof), 0x flamegraphs, autocannon load-driven profiling, Heap snapshots & three-snapshot memory-leak hunting (--heapsnapshot-signal, --heapsnapshot-near-heap-limit, retainer analysis), Inspector / Chrome DevTools Protocol (--inspect/--inspect-brk/--inspect-wait, inspector.Session, CDP over WebSocket), Programmatic profiling via node:inspector (Profiler.* / HeapProfiler.* domains), V8 profilers as CLI flags (--prof/--prof-process tick processor, --cpu-prof, --heap-prof), diagnostics_channel + TracingChannel (zero-idle-cost instrumentation, built-in http/net channels, bindStore), perf_hooks measurement (PerformanceObserver, monitorEventLoopDelay histogram, eventLoopUtilization for pool sizing, timerify, createHistogram)