Writing Heap Snapshots Near the Heap Limit

A Node.js service crashes every few days with FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory, and by the time anyone looks, the evidence is gone. This guide from Memory Limits and Out-of-Heap Errors in Node.js, in JavaScript Memory Fundamentals & Runtime Mechanics, shows how to make Node write a heap snapshot automatically as the heap approaches its limit, how to capture a diagnostic report at the moment of the crash, and how to avoid the snapshot itself killing the container.

Symptom Root Cause Immediate Action Measurable Impact
OOM crash with no heap snapshot Nothing captured before the process died Start with --heapsnapshot-near-heap-limit=1 A .heapsnapshot written just before the crash
Snapshot written but process killed by the container Writing a snapshot needs roughly heap-size extra memory Leave container headroom or lower --max-old-space-size Snapshot completes; container not OOMKilled
Snapshots fill the disk Several snapshots written in a crash loop Limit the count (the flag’s value) and write to a dedicated directory Bounded disk use
No record of what was running at crash time No diagnostic report Add --report-on-fatalerror JSON report with stack, heap stats and resource usage
Cannot restart with new flags Flags fixed in deployment Call v8.setHeapSnapshotNearHeapLimit() at startup Same capability without a flag change

Root Cause: The Evidence Dies With the Process

When V8 cannot satisfy an allocation even after a full garbage collection, and the heap is at its configured limit, it reports a fatal out-of-memory error and the process aborts. Everything that could explain the crash — which objects filled the heap and what retained them — is in the heap that is about to disappear. Heap snapshots are the tool for that question, as covered in why a Node.js process hits the heap limit, but taking one manually requires catching the process before it dies.

Node.js automates that with --heapsnapshot-near-heap-limit=N. When V8 signals that the heap is close to its limit, Node writes a heap snapshot (up to N times over the process lifetime) before V8 gives up. To make this possible, V8 temporarily raises its heap limit so that the snapshot can be generated, then the process usually continues until the real out-of-memory error. The same behaviour can be enabled at runtime with v8.setHeapSnapshotNearHeapLimit(N), which is useful when you cannot change the command line.

There is an important catch: generating a snapshot needs memory — roughly as much again as the heap being serialised, for the snapshot’s internal data structures and output buffers. On a machine with spare memory that is fine. In a container whose memory limit is only slightly above --max-old-space-size, the process can be killed by the kernel’s OOM killer while writing the snapshot, leaving a truncated file and an OOMKilled status instead of a V8 error — the situation described in fixing OOMKilled Node.js containers in Kubernetes. Plan headroom specifically for this, or deliberately run a canary with a lower heap limit so the snapshot is smaller and fits.

Complement the snapshot with a diagnostic report: --report-on-fatalerror makes Node write a JSON report when a fatal error occurs, containing the JavaScript and native stacks, heap statistics per space, resource usage, loaded libraries and environment details. The report is small and always fits; the snapshot is large and may not. Together they cover “what was the heap made of?” and “what was happening at the moment it failed?”.

Near-limit snapshot and crash report timeline The heap grows towards the max-old-space-size limit. When V8 signals it is near the limit, Node writes a heap snapshot; V8 raises the limit temporarily and memory use rises by roughly the heap size while the snapshot is serialised. If the container limit is above that peak, the snapshot completes. The process later hits the fatal out of memory error and writes a diagnostic report. container memory limit --max-old-space-size snapshot being written near-limit fatal OOM → report time

Step-by-Step Fix

  1. Enable near-limit snapshots. Add --heapsnapshot-near-heap-limit=1 (or 2 to capture growth between two points) to the start command, or call v8.setHeapSnapshotNearHeapLimit(1) early at startup. Verification: a test script that allocates until OOM writes a Heap.*.heapsnapshot file before crashing.
  2. Direct output to a writable, persistent location. Use --diagnostic-dir=/var/diagnostics (mounted to persistent or collected storage). Verification: snapshot and report files appear in that directory and survive container restarts.
  3. Add a fatal-error report. Add --report-on-fatalerror (and optionally --report-compact). Verification: the OOM test also produces a report.*.json with heap space statistics and the JavaScript stack.
  4. Budget memory for the snapshot. Ensure the container limit exceeds the heap limit by at least the heap size plus normal native memory, or run a dedicated canary with a lower --max-old-space-size so snapshots stay small. Verification: the OOM test in the real container writes a complete snapshot without being OOMKilled.
  5. Collect and analyse. Ship files from the diagnostic directory to storage, open the snapshot in DevTools → Memory → Load, and sort the Summary by retained size. Verification: you can name the constructor and retainer that filled the heap.
  6. Fix, then keep the flags on. After fixing the leak, keep the flags in production at a count of 1 so the next incident also leaves evidence. Verification: runbooks reference where to find the files.
Snapshot and report answer different questions The heap snapshot, hundreds of megabytes to gigabytes, captures every object and reference, answering what filled the heap and who retains it. The diagnostic report, tens of kilobytes, captures stacks, heap space statistics, resource usage, libraries and environment, answering what was running and how memory was distributed at the moment of failure. Heap snapshot (.heapsnapshot) every object and reference hundreds of MB to GB what filled the heap? who retains it? Diagnostic report (.json) stacks, heap spaces, resources tens of KB — always fits what was running? which space was full?

Command and Code Reference

Use case: production start command with crash evidence enabled.

# Snapshot once near the limit, report on fatal error, both into one directory
node \
  --max-old-space-size=1536 \
  --heapsnapshot-near-heap-limit=1 \
  --report-on-fatalerror \
  --report-compact \
  --diagnostic-dir=/var/diagnostics \
  server.js

Use case: enable near-limit snapshots at runtime. Useful when the command line is controlled by a platform you cannot change.

// at the very top of the entry file
const v8 = require('node:v8');
// Write at most one snapshot when the heap approaches its limit.
// Returns early if a limit was already configured via the command-line flag.
v8.setHeapSnapshotNearHeapLimit(1);

Use case: verify the setup with a deliberate OOM in a test environment.

// oom-test.js — node --max-old-space-size=128 --heapsnapshot-near-heap-limit=1 \
//               --report-on-fatalerror --diagnostic-dir=./diag oom-test.js
const hoard = [];
setInterval(() => {
  // retain ~5 MB of distinct objects per tick until the heap limit is reached
  hoard.push(Array.from({ length: 50_000 }, (_, i) => ({ i, s: `row-${i}` })));
}, 10);

Verification and Regression Prevention

The setup works when a deliberate OOM in the same container configuration as production leaves a complete, loadable snapshot and a report in the collected directory. Test it once whenever you change memory limits, base images or orchestration settings; the most common failure is a container limit that silently became too tight for the snapshot to finish.

Treat the files as sensitive — a heap snapshot contains every string in memory, including tokens and personal data — so restrict access to the diagnostic directory and delete files after analysis. Pair crash evidence with early warning: an alert on heap headroom, as in reading V8 heap space statistics in Node.js, gives you the chance to capture a smaller, manual snapshot long before the limit.

The deliberate-OOM drill In the same container configuration as production, trigger a deliberate out-of-memory condition and confirm that a complete, loadable snapshot and a diagnostic report land in the collected directory. Repeat whenever memory limits, base images or orchestration settings change, because the snapshot needs headroom of its own. Prod-like container same limits and flags Deliberate OOM grow the heap on purpose Snapshot complete? loads in DevTools Collected report + snapshot on volume repeat after changing memory limits, base images or orchestration

Edge Cases and Gotchas

Snapshot size equals heap size

A 1.5 GB heap produces a snapshot file of roughly similar size, which can be slow to write and hard to open. See opening heap snapshots too large for DevTools and consider running a canary with a smaller heap to get smaller files.

The process pauses while writing

Serialising a large heap takes seconds to tens of seconds, during which the process does not serve requests. Health checks may fail and the orchestrator may restart the container mid-write. Lengthen liveness timeouts on canaries that are expected to capture snapshots.

Worker threads

The flag applies to the main thread’s isolate. For workers, set limits via resourceLimits and capture snapshots with worker.getHeapSnapshot() or from inside the worker.

Crash loops

A service that restarts and OOMs repeatedly can write a snapshot per restart. Keep the count at 1 per process, rotate or collect the directory, and alert on the number of files so disks do not fill.

Frequently Asked Questions

What does --heapsnapshot-near-heap-limit do?

It tells Node.js to write a heap snapshot when V8 reports that the heap is close to its limit, up to the number of times given. V8 temporarily raises the limit so the snapshot can be produced, giving you a picture of the heap just before an out-of-memory crash.

Why did my container die while writing the snapshot?

Generating a snapshot needs roughly as much additional memory as the heap itself. If the container’s memory limit leaves no room for that, the kernel kills the process. Increase the container limit relative to --max-old-space-size, or use a lower heap limit on a canary.

What is in a Node.js diagnostic report?

A JSON document with the JavaScript and native stack traces, heap statistics per space, libuv handles, resource usage, loaded shared libraries, environment variables and system information at the time of the event. It is small and quick to write, which makes it a reliable complement to a snapshot.

Should I keep these flags in production permanently?

Yes, with a snapshot count of 1 and a collected, access-controlled output directory. The overhead is zero until the heap nears its limit, and the evidence is invaluable when an out-of-memory crash happens.