In high-concurrency production environments, traditional debugging methods like verbose logging (console.log) or attaching heavyweight interactive debuggers are categorically prohibited. Attaching an interactive debugger with breakpoints freezes the single-threaded Event Loop, causing immediate Kubernetes liveness probe failures and triggering cascading pod restarts across the cluster.
Live production diagnostics rely on non-intrusive methodologies with near-zero runtime overhead (under 2% CPU overhead):
Doctor, Flame, Bubbleprof) leveraging Node.js internal telemetry hooks (async_hooks, perf_hooks) to correlate I/O latencies, Event Loop lag, and active system handle counts.lldb paired with the llnode plugin, engineers reconstruct the exact JavaScript call stack and variable state of the terminated process.Diagnostic tooling splits across process execution lifecycles: live instance online telemetry versus post-mortem investigation of terminated crash artifacts.
[ Node.js Production Instance ]
|
+---------------+---------------+
| (Process is Running) | (Fatal Crash / SegFault)
v v
[ Diagnostics Agent ] [ Linux Kernel ]
- node:inspector - ulimit -c unlimited
- v8.writeHeapSnapshot - kernel.core_pattern
| |
+---------------+ v
| | [ core.[pid] Dump File ]
v v |
[ CPU Profile ] [ Heap Snapshot ] v
(.cpuprofile) (.heapsnapshot) [ lldb + llnode ]
| | |
v v v
[ Clinica Flame / Chrome DevTools ] [ Inspect JS Stack & Memory ]Anatomy of a Flamegraph:
v8.writeHeapSnapshot()) via POSIX SIGUSR2 signals across 1-hour intervals, performing differential comparisons in Chrome DevTools to locate uncollected event listeners.lldb/llnode when image processing microservices (sharp / libvips) crash due to corrupted memory pointers.Clinica Bubbleprof to identify slow, chained relational queries that fracture asynchronous execution pipelines.Production-grade diagnostics require an in-process agent managing node:inspector sessions, streaming heap serialization, and signal interception.
import inspector from "node:inspector";
import fs from "node:fs";
import path from "node:path";
import v8 from "node:v8";
export class ProductionDiagnosticsAgent {
private session: inspector.Session | null = null;
private isProfiling = false;
private readonly storageDir: string;
constructor(storageDir = "/tmp/diagnostics") {
this.storageDir = storageDir;
if (!fs.existsSync(this.storageDir)) {
fs.mkdirSync(this.storageDir, { recursive: true });
}
}
/**
* Captures statistical CPU profile over a specified duration without process restart
*/
public async captureCpuProfile(durationSeconds = 10): Promise<string> {
if (this.isProfiling) {
throw new Error("CPU profiling session is already in progress");
}
this.isProfiling = true;
const session = new inspector.Session();
this.session = session;
session.connect();
return new Promise<string>((resolve, reject) => {
// Enable V8 Profiler domain
session.post("Profiler.enable", (err) => {
if (err) {
this.cleanupSession();
return reject(err);
}
// Set sampling interval in microseconds (1000 µs = 1 ms)
session.post("Profiler.setSamplingInterval", { interval: 1000 }, (intervalErr) => {
if (intervalErr) {
this.cleanupSession();
return reject(intervalErr);
}
// Start sampling execution stacks
session.post("Profiler.start", (startErr) => {
if (startErr) {
this.cleanupSession();
return reject(startErr);
}
console.info(`[Diagnostics] CPU Profiling started for ${durationSeconds}s...`);
// Await profiling duration
setTimeout(() => {
session.post("Profiler.stop", (stopErr, { profile }) => {
this.cleanupSession();
if (stopErr) {
return reject(stopErr);
}
const filename = `cpu-profile-${process.pid}-${Date.now()}.cpuprofile`;
const fullPath = path.join(this.storageDir, filename);
fs.writeFileSync(fullPath, JSON.stringify(profile));
console.info(`[Diagnostics] CPU Profile saved to ${fullPath}`);
resolve(fullPath);
});
}, durationSeconds * 1000);
});
});
});
});
}
/**
* Captures an instant heap snapshot serialized directly to disk
*/
public captureHeapSnapshot(): string {
const filename = `heap-snapshot-${process.pid}-${Date.now()}.heapsnapshot`;
const fullPath = path.join(this.storageDir, filename);
console.info(`[Diagnostics] Streaming Heap Snapshot to ${fullPath}...`);
// Native V8 method: Streams memory directly to disk via C++ runtime, bypassing JS heap allocations
v8.writeHeapSnapshot(fullPath);
console.info(`[Diagnostics] Heap Snapshot completed: ${fullPath}`);
return fullPath;
}
private cleanupSession(): void {
if (this.session) {
this.session.post("Profiler.disable");
this.session.disconnect();
this.session = null;
}
this.isProfiling = false;
}
}import { ProductionDiagnosticsAgent } from "./diagnostics-agent";
export function initializeDiagnosticsSignals(): void {
const agent = new ProductionDiagnosticsAgent();
// SIGUSR2: Trigger on-demand Heap Snapshot without service interruption
process.on("SIGUSR2", () => {
try {
console.warn(`[Process ${process.pid}] Received SIGUSR2: triggering Heap Snapshot`);
const snapshotPath = agent.captureHeapSnapshot();
console.warn(`[Process ${process.pid}] Heap Snapshot ready: ${snapshotPath}`);
} catch (error) {
console.error("[Diagnostics] Failed to capture snapshot on SIGUSR2:", error);
}
});
console.info(`[Diagnostics] Signal listeners attached to PID ${process.pid}. Send SIGUSR2 for Heap Snapshot.`);
}Process encounters SIGSEGV or uncaught exception under --abort-on-uncaught-exception.
Linux kernel flushes virtual memory pages to disk following kernel.core_pattern.
Engineer launches LLDB targeting the original Node.js binary and generated core file.
Loads the llnode plugin to decode C++ memory structures into JavaScript runtime types.
Executes v8 bt to reconstruct the exact JavaScript execution call stack and inspect local variables.
FROM node:20-bookworm-slim
# Install system dependencies required for LLDB and llnode compilation
RUN apt-get update && apt-get install -y \
lldb \
gdb \
python3 \
python3-pip \
make \
g++ \
curl \
&& rm -rf /var/lib/apt/lists/*
# Install llnode globally to inspect V8 memory structures
RUN npm install -g llnode --unsafe-perm
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
# Create writable directory for core dump generation
RUN mkdir -p /var/cores && chmod 777 /var/cores
# Runtime Node.js flags:
# --abort-on-uncaught-exception: Force core dump generation on unhandled exceptions
# --diagnostic-report-on-fatalerror: Simultaneously generate JSON diagnostic report
ENV NODE_OPTIONS="--abort-on-uncaught-exception --diagnostic-report-on-fatalerror --diagnostic-report-path=/var/cores"
CMD ["node", "dist/main.js"]Using legacy packages like heapdump that buffer snapshot data inside JavaScript arrays prior to disk write. If a process is already consuming 1.6 GB out of a 2 GB container memory ceiling, buffering the snapshot internally requires an additional 1 GB of heap. The Linux kernel OOM-Killer terminates the container before the file finishes writing. Solution: Always use v8.writeHeapSnapshot(), which streams serialized data directly to disk via C++ bindings with negligible JavaScript memory overhead.
Executing clinica bubbleprof in production under 5,000 RPS. Bubbleprof intercepts every asynchronous operation via async_hooks. Under dense micro-task loads, the tracking overhead increases application latency by 30–50%, triggering service degradation. Reserve Bubbleprof for staging environments, load tests, or isolated canary pods.
Capturing CPU profiles on production bundles where TypeScript was minified without source maps (source-map-support). In the flamegraph, all stack frames appear as anonymous minified functions like e(t, r). Always retain meaningful function identifiers or configure hidden source maps for diagnostic builds.
In standard Docker and Kubernetes container runtimes, the default core dump file size limit is set to zero (ulimit -c 0). When a native segmentation fault occurs, no core dump file is created. To enable post-mortem analysis, explicitly declare ulimits: { core: -1 } inside the container security specification.
Detect spikes in Event Loop Delay or CPU utilization via Prometheus and Grafana alerts
Trigger 10-second sampling CPU profile via ProductionDiagnosticsAgent without restarting pods
Import .cpuprofile into Chrome DevTools or Clinica Flame to identify flattop CPU bottlenecks
Capture paired Heap Snapshots 1 hour apart to isolate growing object constructor allocations
Deploy algorithmic optimizations and verify normalization of p99 latency metrics