What this module covers: When a Node.js process crashes with an uncaught exception, OOM kill, or fatal error, you typically have one chance to capture diagnostic information before the process terminates. process.report generates a structured JSON diagnostic report containing the V8 heap state, libuv handle and request queues, native C++ call stacks, environment variables, and system information — captured at the exact moment of failure. This module covers configuring diagnostic reports for production, integrating report generation into SRE alert pipelines, and reading the output to diagnose crashes that are otherwise invisible.
process.report is Node's flight data recorder — you never want to need it, but when the process goes down, it's the only artifact that survives to tell you what the engines looked like in the final second.
A process.report output is a JSON file with these sections:
In one file, you have: the exact error, the V8 heap state at failure time, every open file descriptor (TCP connections, sockets), environment variables, and CPU usage — without needing to attach a debugger or reproduce the crash.
Operational caveat: report generation can itself fail under severe OOM. Writing a report requires allocating memory to serialize the heap and handle state to JSON — if the process is already at the absolute edge of its memory limit when the fatal error fires, there may not be enough headroom left to produce the report at all, or the report may be truncated mid-write (a partial JSON file, or one missing the javascriptHeap section entirely). This is not a reason to skip reportOnFatalError — a truncated report with a stack trace is still far more useful than nothing — but don't assume the report is guaranteed to be complete during the worst OOM events, which are exactly the events where you need it most.
This is a real risk, not a hypothetical one.process.report does NOT redact anything by default — environmentVariables above is the raw, unmodified process.env, verbatim. If your process has DATABASE_URL, API_KEY, JWT secrets, or any other credential in its environment (which is the normal way to configure a production Node.js service), every diagnostic report contains those credentials in plaintext. A report generated on OOM or uncaught exception, written to disk and then uploaded to S3 or attached to a ticket, is a credential leak waiting to happen unless you explicitly redact it first — see the redaction step below, which must run before the report is persisted or shipped anywhere.
// Configure automatic report generation at application startup
// Report on any fatal error (OOM, uncaught exception, SIGTERM) process.report.reportOnFatalError = true;
// Report on uncaught exceptions (before process exits) process.report.reportOnUncaughtException = true;
// Report on SIGUSR2 signal (manual trigger without process restart) process.report.reportOnSignal = true; process.report.signal = 'SIGUSR2'; // default
// Where to write reports process.report.directory = '/app/diagnostic-reports'; process.report.filename = 'report.{date}.{time}.{pid}.json'; // Variables available: {date}, {time}, {pid}, {tid}, {hostname}, {timestamp}
# Trigger a report manually without restarting kill -SIGUSR2 $(pgrep -f "node dist/app.js") # Generates: /app/diagnostic-reports/report.20260517.143022.12847.json # Process continues running — zero impact on production traffic
Every option above can also be set as a startup flag, which is often preferable in containerized deployments where you want the behavior locked in from process launch — before any application code has a chance to run (or fail to run):
# Equivalent to setting the properties programmatically at startup, but active # from the very first tick — including crashes during module initialization, # before your own code would have had a chance to set process.report.* at all node \ --report-uncaught-exception \ --report-on-fatalerror \ --report-on-signal \ --report-directory=/app/diagnostic-reports \ --report-filename=report.{date}.{time}.{pid}.json \ --report-compact \ dist/app.js
Flag
Equivalent to
--report-uncaught-exception
process.report.reportOnUncaughtException = true
--report-on-fatalerror
process.report.reportOnFatalError = true
--report-on-signal
process.report.reportOnSignal = true
--report-directory=<dir>
process.report.directory = '<dir>'
--report-compact
Single-line JSON (no pretty-printing) — smaller files, easier to pipe into log aggregators that expect one JSON object per line
For a Dockerized indexer, baking these flags into the container's ENTRYPOINT/CMD (or a NODE_OPTIONS env var) guarantees report generation is armed regardless of how the application code initializes — including if the crash happens before your own process.report.reportOnFatalError = true line would have executed.
Because environmentVariables is raw process.env, every report must be scrubbed before it is written anywhere durable or uploaded anywhere. Never skip this step — an unredacted report is a plaintext credential dump.
// redact-report.js import { readFileSync, writeFileSync } from 'node:fs';
// Keys matched case-insensitively. Extend this list for your own secret naming conventions. const SENSITIVE_KEY_PATTERNS = [ /_SECRET$/i, /_KEY$/i, /_TOKEN$/i, /_PASSWORD$/i, /^PASSWORD$/i, /_URL$/i, // catches DATABASE_URL, REDIS_URL, etc. (may embed creds) /_DSN$/i, /_CREDENTIALS?$/i, /^AWS_/i, /_API_KEY$/i, ];
function isSensitiveKey(key) { return SENSITIVE_KEY_PATTERNS.some((pattern) => pattern.test(key)); }
// Redact sensitive process.env entries inside a parsed report object, in place. function redactReportObject(report) { if (report.environmentVariables && typeof report.environmentVariables === 'object') { for (const key of Object.keys(report.environmentVariables)) { if (isSensitiveKey(key)) { report.environmentVariables[key] = '[REDACTED]'; } } } return report; }
// Read a report from disk, redact it, and overwrite it with the scrubbed version. // Call this immediately after process.report.writeReport() — before any upload, // log line, or ticket attachment. function redactReportFile(reportPath) { const report = JSON.parse(readFileSync(reportPath, 'utf8')); redactReportObject(report); writeFileSync(reportPath, JSON.stringify(report, null, 2)); return reportPath; }
// Scrub secrets from process.env BEFORE the report leaves this process redactReportFile(reportFilename); logger.info({ reportFilename }, 'Diagnostic report written and redacted before shutdown');
// Copy the REDACTED report to persistent storage (pod storage is ephemeral) await uploadToS3(reportFilename, `reports/${process.pid}-${Date.now()}.json`);
// Then graceful shutdown await gracefulShutdown('SIGTERM'); });
Production story: during a UPI settlement spike, indexer pods started restarting repeatedly under memory pressure — but the logs showed nothing. Kubernetes was sending SIGKILL directly, with no SIGTERM grace period configured, which meant the process was terminated before its logger could flush the last lines explaining what was going wrong. Every restart looked identical from the outside: pod dies, pod restarts, no error in the logs, repeat. The crash was completely unreproducible in staging because staging never carried the same memory pressure.
The fix was exactly the pattern above: process.report.reportOnFatalError = true plus a SIGTERM handler that writes and uploads a redacted report to S3 before the pod's ephemeral storage disappears. Once wired up, the very next crash produced a report showing hundreds of leaked Redis client handles in the libuv section — a subscription client was being re-created on every reconnect attempt without closing the previous one. What had been an unreproducible, silent, recurring crash became a five-minute diagnosis from a single artifact.
// From the report's javascriptHeap section: { "heapSpaces": { "old_space": { "memorySize": 2147483648, // 2GB allocated "usedMemory": 2145001472, // 2GB used = 99.9% full → OOM }, "new_space": { "usedMemory": 33501200, // new space mostly full too } } }
// Diagnosis: Old Space completely full. Likely cause: memory leak. // Next step: compare with two heap snapshots (Module 12 runbook)
// From the report's header + uvthreadResourceUsage: { "header": { "event": "Signal", "trigger": "SIGUSR2" }, "uvthreadResourceUsage": { "userCpuSeconds": 847, "kernelCpuSeconds": 0.1 // very low kernel time } }
// High user CPU, very low kernel CPU: // Node.js is burning CPU in JavaScript (not I/O) // Combined with ELU > 0.95: pure JavaScript computation blocking the loop // Next step: clinic flame to identify the function
// Configure crash reporting pipeline import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; import { createReadStream } from 'fs'; import { redactReportFile } from './redact-report.js';
const s3 = new S3Client({ region: 'ap-south-1' });
async function uploadDiagnosticReport(filename) { // Redact secrets from process.env BEFORE the file is ever read for upload. // This must happen before createReadStream — the file on disk itself is // rewritten scrubbed, so nothing unredacted is ever sent to S3. redactReportFile(filename);
Every example so far has used process.report.writeReport(), which writes the report to disk as a file — useful when you need a persistent artifact to upload or attach to a ticket. But when all you want is to inspect or triage the current process's diagnostic state in-process, writing to disk and immediately reading it back is an unnecessary round trip. process.report.getReport() returns the same report as a plain JavaScript object, entirely in memory:
// Direct, in-memory alternative to writeReport() + readFileSync() + JSON.parse() // — useful for a health-check endpoint or an in-process triage function that // doesn't need a file on disk at all function analyzeCurrentProcess() { const report = process.report.getReport(); // no disk I/O, no file to clean up
// e.g. exposed on an internal diagnostics endpoint for live triage, // without ever touching the filesystem app.get('/internal/diagnostics', (req, res) => { res.json(analyzeCurrentProcess()); });
Use getReport() for live, in-process inspection (health endpoints, ad-hoc debugging in a REPL, in-process alerting logic). Use writeReport() when you need a durable file — for post-mortem analysis after the process has already died, writeReport() (typically paired with reportOnFatalError/reportOnSignal) is the only option, since there's no "current process" left to call getReport() on. The CI/CD analysis below reads a report that was already written to disk by a previous, separate process run — that's the case where re-parsing a file is unavoidable, not a missed opportunity to use getReport().
// Read and parse a report for automated analysis import { readFileSync } from 'fs';
function analyzeReport(reportPath) { const report = JSON.parse(readFileSync(reportPath, 'utf8'));
// Automated triage if (analysis.heapUsedPct > 95) analysis.likelyCause = 'OOM / memory leak'; if (analysis.openTcpConnections > 200) analysis.likelyCause = 'Connection leak'; if (analysis.cpuTimeSeconds > 3600) analysis.likelyCause = 'Long-running process with CPU issue';
return analysis; }
// Use in CI/CD to detect memory leaks during load tests const analysis = analyzeReport('./diagnostic-reports/latest.json'); if (analysis.heapUsedPct > 80) { throw new Error(`Memory leak detected: heap at ${analysis.heapUsedPct}%`); }
Structured JSON at the moment of failure. V8 heap, libuv handles, call stacks, raw (unredacted) env vars.
reportOnFatalError
Captures report on OOM, segfault, other fatal errors. No code change needed after configuration.
reportOnSignal
SIGUSR2 triggers a report in a running process. Zero impact on traffic.
writeReport()
Programmatic report generation in signal handlers and error handlers.
Heap section
usedMemory / totalMemory > 95% = OOM. Old Space full = memory leak.
libuv section
400 TCP connections to one host = connection leak. Unexpected handles = resource leak.
CPU section
High user CPU, low kernel CPU = JavaScript blocking the event loop.
Redact before persisting
environmentVariables is raw process.env — never pre-redacted. Run redactReportFile() before writing to disk, uploading, or linking in alerts.
S3 upload before exit
Persist redacted reports from ephemeral Kubernetes pods before they disappear.
Automated analysis
Parse reports in CI/CD load tests to detect memory leaks before production.
Next: HTTP/2, gRPC Transport, and Protocol Selection →
Knowledge Check
How can you configure Node.js to automatically capture the V8 heap state and open file descriptors exactly when the process crashes from an Out of Memory (OOM) error?
What does the presence of hundreds of identical outgoing TCP connection entries (e.g., { "type": "tcp", "address": "db.internal", "port": 5432 }) in the libuv section of a process.report most likely indicate?
What is the recommended way to reliably capture and preserve a process.report when Kubernetes shuts down a pod via a SIGTERM signal?
Test your knowledge with more question sets
Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.