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 inspector module lets code open an in-process inspector session and send HeapProfiler.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.

A safe capture on a live instance Step one, remove the instance from the load balancer so no user traffic hits it. Step two, trigger the snapshot with a signal or admin call. Step three, V8 runs a full GC and serialises the heap; the event loop is blocked and memory rises by roughly the heap size. Step four, upload the file to secured storage and delete it locally. Step five, restart or return the instance to rotation. 1. Drain out of rotation 2. Trigger signal / admin call 3. GC + serialise event loop blocked memory + ~heap size 4. Upload secure storage 5. Restore or restart container memory limit must exceed normal RSS + roughly the heap size take two snapshots minutes apart on the same instance to diff growth

Step-by-Step Fix

  1. Enable a capture path before you need it. Add --heapsnapshot-signal=SIGUSR2 and --diagnostic-dir=/var/diagnostics to the start command, or ship an authenticated admin endpoint that calls v8.writeHeapSnapshot(). Verification: in staging, sending the signal writes a Heap.*.heapsnapshot file.
  2. 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.
  3. 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.
  4. Move files off the host. Copy them to secured storage (for example with kubectl cp or an upload step), then delete them locally. Verification: files are in storage with restricted access; the pod’s disk is clean.
  5. 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.
  6. 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.
RSS during a snapshot of a 600 MB heap A process with 900 megabytes RSS and a 600 megabyte heap rises to about 1.5 gigabytes while the snapshot is being generated and written over about 20 seconds, then falls back to around 950 megabytes. A container limit of 1.2 gigabytes would kill the process during capture. 1.6 GB 0 1.2 GB container limit (too low) capture ~20 s time

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.

A safe capture in production Drain the instance from the load balancer, confirm there is memory headroom for the snapshot (it can need as much again as the heap), capture with the heap snapshot signal or inspector, move the file off the host, and restore traffic. Rehearse in staging with the same limits. Drain remove from load balancer Headroom? up to heap size again Capture signal or inspector Restore move file off, re-add rehearse in staging with the same container limits and heap size

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.