Taking Heap Snapshots from a Live Node.js Process
A production service’s memory is growing and you need to see what is on its heap now — without restarting it with new flags, without attaching a debugger to a production port, and without the snapshot itself killing the pod. This guide from Diagnosing Node Memory with Heapdump and Clinic, part of Node.js Server-Side Memory Management, covers every built-in way to capture a heap snapshot from a running Node.js process, what each costs, and a safe routine for doing it in containers.
| Symptom | Root Cause | Immediate Action | Measurable Impact |
|---|---|---|---|
Need a snapshot but cannot restart with --inspect |
No capture mechanism built in | Use --heapsnapshot-signal on next deploy, or an admin-triggered v8.writeHeapSnapshot() |
Snapshots on demand, no debugger port |
| Pod killed while writing a snapshot | Snapshot needs roughly heap-size extra memory | Capture on an instance with headroom, or lower heap on a canary | Snapshot completes |
| Health checks fail during capture | Snapshot writing blocks the event loop | Drain the instance from the load balancer first | No user-facing errors |
| Worker threads’ memory invisible | Snapshots cover one isolate | Use worker.getHeapSnapshot() or capture inside the worker |
Per-worker visibility |
| Snapshots contain secrets | Heap includes tokens, PII, keys | Treat files as sensitive; restrict and delete | Compliance maintained |
Root Cause: A Snapshot Is a Stop-the-World Serialisation of the Heap
A heap snapshot records every object in a V8 isolate, every reference between them and every string, in the .heapsnapshot JSON format that Chrome DevTools reads. To produce it, V8 runs a full garbage collection, walks the whole heap and serialises it. That has three consequences for live services. First, the event loop is blocked for the duration — seconds for a few hundred megabytes, tens of seconds for gigabytes — so requests stall and health checks may fail. Second, memory use spikes: building and writing the snapshot needs additional memory roughly on the order of the heap itself, which can push a container over its limit, the same risk discussed in writing heap snapshots near the heap limit. Third, the file contains everything in memory, including credentials and user data.
Node.js offers several capture mechanisms, all producing the same format:
v8.writeHeapSnapshot([filename])writes a snapshot synchronously to disk and returns the file name. You can call it from an admin-only HTTP endpoint or a signal handler.--heapsnapshot-signal=SIGUSR2(any signal) makes Node write a snapshot when the process receives that signal, with no code changes — the easiest option to enable ahead of time.v8.getHeapSnapshot()returns a readable stream of the snapshot, useful for streaming to object storage instead of local disk.- The
inspectormodule lets code open an in-process inspector session and sendHeapProfiler.takeHeapSnapshot, the same protocol Chrome DevTools uses, without exposing a network port. worker.getHeapSnapshot()captures a worker thread’s isolate from the main thread; each worker has a separate heap, as explained in worker threads memory isolation.
Remote attachment with --inspect and Chrome DevTools is also possible but opens a debugging port; it is covered separately in attaching Chrome DevTools to a remote Node.js process. For comparing tools more broadly, see heapdump vs Clinic vs node --inspect.
Step-by-Step Fix
- Enable a capture path before you need it. Add
--heapsnapshot-signal=SIGUSR2and--diagnostic-dir=/var/diagnosticsto the start command, or ship an authenticated admin endpoint that callsv8.writeHeapSnapshot(). Verification: in staging, sending the signal writes aHeap.*.heapsnapshotfile. - Pick an instance with headroom and drain it. Choose an instance whose RSS plus roughly its heap size fits under the container limit, and remove it from the load balancer. Verification: the instance receives no user traffic.
- Capture a baseline, wait, capture again. Trigger one snapshot, let the leak-driving traffic run (or replay it) for several minutes, then trigger a second. Verification: two files of increasing size exist.
- Move files off the host. Copy them to secured storage (for example with
kubectl cpor an upload step), then delete them locally. Verification: files are in storage with restricted access; the pod’s disk is clean. - Analyse the difference. Load both snapshots into DevTools → Memory → Load and use the Comparison view, or the three-snapshot technique with a third capture. Verification: the growing constructors and their retainers are identified.
- Return or replace the instance. Put it back into rotation or restart it; the capture itself may have left memory fragmented. Verification: the instance serves traffic normally.
Command and Code Reference
Use case: signal-triggered snapshots in Kubernetes.
# Deployment start command (no code changes):
# node --heapsnapshot-signal=SIGUSR2 --diagnostic-dir=/var/diagnostics server.js
POD=api-7c9d6c5b8-x2k4q
kubectl exec "$POD" -- sh -c 'kill -USR2 1' # PID 1 is node in most images
sleep 30 # wait for the write to finish
kubectl exec "$POD" -- ls -lh /var/diagnostics
kubectl cp "$POD":/var/diagnostics/ ./snapshots/ # copy off the pod
kubectl exec "$POD" -- sh -c 'rm -f /var/diagnostics/*.heapsnapshot'
Use case: an authenticated admin endpoint that streams a snapshot to storage.
// admin.js — mount only on an internal admin port, behind authentication
const v8 = require('node:v8');
const { pipeline } = require('node:stream/promises');
adminApp.post('/admin/heap-snapshot', requireAdmin, async (req, res) => {
const key = `heap/${process.env.HOSTNAME}-${Date.now()}.heapsnapshot`;
// getHeapSnapshot() returns a Readable; the event loop still blocks while V8 serialises
await pipeline(v8.getHeapSnapshot(), storage.createWriteStream(key));
res.json({ key });
});
Use case: capture a worker thread’s heap from the main thread.
const { pipeline } = require('node:stream/promises');
const fs = require('node:fs');
async function snapshotWorker(worker, file) {
const stream = await worker.getHeapSnapshot(); // readable stream for the worker isolate
await pipeline(stream, fs.createWriteStream(file));
}
Verification and Regression Prevention
The capture routine works when a drill in staging — same container limits, same heap size — produces complete, loadable snapshots without the pod being killed or health checks failing for real users. Run the drill after changing memory limits or base images. Keep the procedure in a runbook with the exact commands, the drain step and the storage location.
Pair on-demand snapshots with automatic evidence: --heapsnapshot-near-heap-limit for out-of-memory crashes and an alert on heap growth slope (see alerting on memory leaks with growth slope) so engineers capture snapshots while the leak is still small. Smaller heaps mean faster captures, smaller files and less risk.
Edge Cases and Gotchas
Signal handlers in the application
If your application installs its own SIGUSR2 handler (some process managers use it for reloads), the signal will do both or conflict. Choose a signal nothing else uses for --heapsnapshot-signal. Avoid SIGUSR1, which Node reserves for activating the inspector.
PID 1 and signal delivery
In containers, Node often runs as PID 1. Signals are delivered normally when Node is PID 1, but if a shell or init wrapper is PID 1, send the signal to the Node process’s actual PID.
Snapshots include garbage-free data only
Because V8 collects garbage before serialising, snapshots show only reachable objects. Growth in the second snapshot is therefore retention, not uncollected garbage.
Large files and slow disks
Writing gigabytes to a slow or network-backed volume extends the pause. Prefer local ephemeral storage for the write, then upload, or stream directly to object storage.
Frequently Asked Questions
How do I take a heap snapshot of a running Node.js process without restarting it?
If the process was started with --heapsnapshot-signal, send it that signal. Otherwise you need code already running in the process — an admin endpoint calling v8.writeHeapSnapshot() or an inspector session — or a restart with a capture mechanism enabled. Enable one in advance for every service.
Does taking a heap snapshot pause the application?
Yes. V8 performs a full garbage collection and serialises the heap on the main thread, blocking the event loop for seconds to tens of seconds depending on heap size. Drain the instance from traffic before capturing.
Why was my container killed during a snapshot?
Generating the snapshot requires additional memory roughly proportional to the heap. If RSS plus that overhead exceeds the container limit, the kernel kills the process. Capture on instances with headroom or run a canary with a smaller heap.
Can I take snapshots of worker threads?
Yes. worker.getHeapSnapshot() from the main thread returns a stream of the worker’s heap, or you can call v8.writeHeapSnapshot() inside the worker. Each worker has its own isolate and needs its own snapshot.
How many snapshots do I need?
Two snapshots minutes apart on the same instance, under the traffic that drives the growth, are enough for a comparison. A third lets you use the three-snapshot technique to separate warm-up allocations from true accumulation. Taking many more rarely helps and multiplies the pauses and storage.
Are heap snapshots safe to share?
Treat them as sensitive. They contain every string in memory, including tokens, secrets and personal data. Store them with restricted access, share only with people who need them, and delete them after the investigation.
Related
- Diagnosing Node Memory with Heapdump and Clinic — the parent topic
- Using --heap-prof Sampling in Production — a lower-overhead alternative for allocation data
- Opening Heap Snapshots Too Large for DevTools — analysing multi-gigabyte captures
- Node.js Server-Side Memory Management — the section overview