NodeJSDiagnostics & Diagnostic Report

Diagnostics & Diagnostic Report

Interactive debugging assumes you can attach to a process and reproduce the problem. Production incidents rarely cooperate: a process crashes at 3am, hangs intermittently, or leaks memory over days. For these, Node provides diagnostics — tooling that captures the runtime's state for after-the-fact analysis. The headline feature is the Diagnostic Report: a single JSON file summarizing the process's stacks, resource usage, and environment at a moment of failure. This page covers diagnostic reports, the broader diagnostics toolbox (async_hooks, trace events, perf_hooks, heap/CPU profiles), and how to wire them up for production.

The Diagnostic Report

Bash
# Auto-generate a report whenever the process crashes (uncaught exception):
node --report-on-fatalerror --report-uncaught-exception app.js

# Also trigger on a fatal signal (e.g. SIGUSR2) for a hung process:
node --report-on-signal --report-signal=SIGUSR2 app.js

# Control where reports are written:
node --report-directory=/var/log/node-reports app.js

Trigger a report programmatically

JS
// Write a report on demand — e.g. inside an error handler or a health probe:
process.report.writeReport()                 // returns the filename
process.report.writeReport('crash-detail.json')

// Or capture it as an object without writing a file:
const report = process.report.getReport()
logger.error({ report: JSON.parse(report) }, 'captured diagnostic report')
A diagnostic report is a JSON snapshot of the whole process at a moment of failure
The Diagnostic Report is a built-in feature (stable since Node 14) that dumps a comprehensive JSON file capturing the process state: JavaScript and native **stack traces** for every thread, **resource usage** (CPU, memory, heap), **libuv handles** (open sockets, timers, file watchers), **environment variables** and command-line flags, OS and Node versions, and the reason the report was generated. It's designed for incidents you *can't* reproduce interactively — configure it to fire automatically on fatal errors and signals, and the next crash leaves behind a complete forensic snapshot instead of just a stack trace.
What's inside a report

Section

Tells you

header

Why/when it was generated, Node version, command line, PID

javascriptStack

The JS call stack at the moment of capture

nativeStack

The C++ stack — useful for native-addon or runtime crashes

javascriptHeap

Heap size, used/available memory, GC stats

libuv

Active handles & requests — leaked sockets, timers, pending I/O

resourceUsage

CPU time, max RSS, page faults

environmentVariables

The process environment (beware: may contain secrets)

Diagnostic reports can contain secrets — they dump environment variables and command-line args
A report includes the full environment and command line, which on most servers means **secrets** — database URLs with passwords, API keys, JWT signing keys passed via env. Treat report files as sensitive: write them to a restricted directory, don't commit them to source control, scrub them before sharing in a ticket or with support, and apply the same access controls you would to logs containing credentials. The `libuv` section is often the most useful for diagnosing hangs and leaks — a steadily growing count of active handles points straight at sockets or timers that are never being closed.
The wider diagnostics toolbox

Tool

Purpose

Invoke

Diagnostic report

Process snapshot on crash/hang

--report-* flags, process.report

CPU profile

Where CPU time goes

--cpu-prof.cpuprofile

Heap snapshot

What's retained in memory

--heapsnapshot-near-heap-limit, v8.writeHeapSnapshot()

Trace events

Timeline of V8/Node internal events

--trace-event-categories

perf_hooks

Measure your own timings & event-loop lag

perf_hooks module

async_hooks

Track async resource lifecycle

async_hooks module (low-level)

Auto heap snapshot when memory is about to blow — invaluable for OOM crashes

JS
// Writes a heap snapshot just before the process hits the heap limit:
//   node --heapsnapshot-near-heap-limit=2 app.js
// Or write one on demand:
import v8 from 'node:v8'
const file = v8.writeHeapSnapshot()   // load the .heapsnapshot in DevTools Memory tab
`--heapsnapshot-near-heap-limit` captures the heap right before an OOM kill — the only chance to see the leak
Out-of-memory crashes are notoriously hard to debug because the process dies before you can inspect it. `--heapsnapshot-near-heap-limit=N` tells V8 to automatically write up to N heap snapshots as it approaches the limit — capturing the bloated heap *at the moment of failure*, which you then load into Chrome DevTools' Memory panel to find what's retained. Pair this with the report-on-fatalerror flags and you can diagnose a production OOM from the artifacts it leaves behind, without ever reproducing it locally. See [Finding Memory Leaks](/nodejs/memory-leaks) for analyzing the snapshot.
Measuring with perf_hooks

JS
import { performance, monitorEventLoopDelay } from 'node:perf_hooks'

// Measure event-loop lag — the key Node health signal:
const h = monitorEventLoopDelay({ resolution: 20 })
h.enable()
setInterval(() => {
  console.log('event-loop p99 lag (ms):', (h.percentile(99) / 1e6).toFixed(1))
}, 5000)

// Time a specific operation precisely:
performance.mark('start')
await doExpensiveWork()
performance.mark('end')
performance.measure('expensive-work', 'start', 'end')
console.log(performance.getEntriesByName('expensive-work')[0].duration, 'ms')
`monitorEventLoopDelay` quantifies the single most important Node health metric in production
`perf_hooks` is the standards-based (`PerformanceObserver`/User Timing) way to measure your application's own behaviour. `monitorEventLoopDelay()` is the standout: it samples how late timers fire, giving you event-loop lag percentiles — the clearest signal that the [single thread](/nodejs/event-loop) is being blocked and every request is suffering. Feed those numbers into your [monitoring](/nodejs/monitoring-apm) and alert when p99 lag climbs. The `mark`/`measure` API gives precise, low-overhead timing of specific operations, which beats sprinkling `Date.now()` differences through your code.
A production diagnostics checklist
  • Enable reports on fatal errors--report-on-fatalerror --report-uncaught-exception so every crash leaves a snapshot.

  • Enable a signal trigger--report-on-signal to capture a hung process on demand without killing it.

  • Set --heapsnapshot-near-heap-limit so OOM crashes leave an analyzable heap.

  • Write artifacts to a persistent, access-controlled volume — not ephemeral container storage, and not world-readable (they hold secrets).

  • Monitor event-loop lag with perf_hooks and alert on the p99.

  • Keep symbol/source maps for the deployed build so captured stacks are readable.

Next
From finding problems to making things fast: [Performance Overview](/nodejs/performance-intro).