Catch Leaks Early with Heap Snapshots Before Your Server OOMs
Memory Tuning and Heap Snapshots
Learn to take heap snapshots, read the retained-size column, raise or lower --max-old-space-size appropriately, and avoid the three most common algorithm memory leaks in JavaScript.
What you'll learn
- Capture a heap snapshot and open it in Chrome DevTools Memory panel
- Explain retained size vs shallow size in a snapshot
- Avoid the three common algorithm memory leaks — large closures, accidental retention, and unbounded caches
A fast algorithm that leaks memory will eventually crash your Node process — usually at 3 a.m. in production. Heap snapshots let you see what is alive on the heap and who is keeping it alive, so you can fix the retention before it becomes a page.
Taking a Heap Snapshot
// snapshot.mjs — programmatic snapshot via v8
import v8 from 'node:v8';
import { writeFileSync } from 'node:fs';
// Simulate a growing cache
const cache = new Map();
for (let i = 0; i < 100_000; i++) {
cache.set(i, { value: i, data: new Array(100).fill(i) });
}
const snapshot = v8.writeHeapSnapshot(); // writes heapdump-<pid>-<ts>.heapsnapshot
console.log('Snapshot written to', snapshot);
// Open in Chrome DevTools → Memory → Load snapshot Alternatively, start Node with --inspect and use the Memory tab of Chrome
DevTools to trigger a snapshot from the running process.
Shallow vs Retained Size
| Term | Definition |
|---|---|
| Shallow size | Bytes used by the object itself (not what it references) |
| Retained size | Total bytes freed if this object were collected — includes everything it exclusively keeps alive |
Sort by retained size to find the biggest memory holders. A 64-byte object with a 200 MB retained size is your leak.
Raising —max-old-space-size
Node defaults to roughly 1.5 GB heap on 64-bit systems. For batch jobs that genuinely need more:
// node --max-old-space-size=4096 batch-job.mjs
// Sets the old-generation heap ceiling to 4 GB
// Only do this for known-heavy batch work — it does not fix a leak Do not raise the limit to paper over a leak in a long-running server. The process will just OOM later and take longer to do so.
Three Common Algorithm Memory Leaks
1. Large closures that capture more than needed
// Leak: the returned function holds a reference to the entire `results` array
function buildFinder(results) {
return (id) => results.find((r) => r.id === id);
}
// Fix: extract only what you need before closing over it
function buildFinder(results) {
const index = new Map(results.map((r) => [r.id, r]));
return (id) => index.get(id); // results can now be GC'd
} 2. Accidental global / module-level retention
// Module-level arrays grow without bound if items are never removed
const processed = []; // bad for long-running servers
export function record(item) {
processed.push(item); // memory grows forever
}
// Fix: use a bounded structure or WeakRef / WeakMap for caches 3. Unbounded memoisation caches
// Naive memo leaks if called with many unique arguments
function memo(fn) {
const cache = new Map();
return (...args) => {
const key = JSON.stringify(args);
if (!cache.has(key)) cache.set(key, fn(...args));
return cache.get(key);
};
}
// Fix: cap the cache size or use a WeakMap when keys are objects Up Next
With performance and memory under control, the next step is pattern recognition — the mental shortcuts that make solving new problems faster.
Pattern Recognition Cheat Sheet →