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?”.
Step-by-Step Fix
- Enable near-limit snapshots. Add
--heapsnapshot-near-heap-limit=1(or2to capture growth between two points) to the start command, or callv8.setHeapSnapshotNearHeapLimit(1)early at startup. Verification: a test script that allocates until OOM writes aHeap.*.heapsnapshotfile before crashing. - 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. - Add a fatal-error report. Add
--report-on-fatalerror(and optionally--report-compact). Verification: the OOM test also produces areport.*.jsonwith heap space statistics and the JavaScript stack. - 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-sizeso snapshots stay small. Verification: the OOM test in the real container writes a complete snapshot without being OOMKilled. - 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.
- 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.
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.
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.
Related
- Memory Limits and Out-of-Heap Errors in Node.js — the parent topic
- Setting max-old-space-size Correctly — sizing the limit these snapshots are triggered by
- Taking Heap Snapshots from a Live Node.js Process — manual capture before things get critical
- JavaScript Memory Fundamentals & Runtime Mechanics — the section overview