Memory grows until the process is killed
“Memory climbs steadily after each deploy, latency worsens as GC works harder, and every few hours the pod is OOMKilled or Node prints "FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory".”
THE PRINCIPLE
Anything with a lifetime longer than a request (module scope, singletons, emitters, timers) must be bounded or explicitly released. Raising the heap limit only moves the crash.
FIRST MOVES
- Confirm the shape: heapUsed that keeps rising after GC across hours is a leak; a high but flat plateau is sizing. Check RSS vs heap to spot Buffers and native memory.
- Take snapshot A after warm-up and B after sustained load (--heapsnapshot-signal or v8.writeHeapSnapshot), compare in DevTools, and follow the retainers of the biggest positive delta.
- Use --heapsnapshot-near-heap-limit to capture evidence automatically before the crash. Snapshots pause the process and can double memory, so use an instance that can safely die.
- Check the usual retainers: unbounded Maps used as caches, listeners added per request (MaxListenersExceededWarning), setInterval closures, and request objects captured in long-lived promises.
- Set --max-old-space-size below the container memory limit so V8 collects before the kernel kills the pod, leaving headroom for native memory.
TOOLS: THEN → NOW
Restarting on a schedule and raising --max-old-space-sizeHeap snapshot comparison in Chrome DevTools to find the retainer
heapdump packageBuilt-in v8.writeHeapSnapshot(), --heapsnapshot-signal, and --heapsnapshot-near-heap-limit
Plain Map cachesBounded LRU/TTL caches, or WeakMap when the entry should live only as long as its key object
PATTERN SNAPSHOT
// Run: node --heapsnapshot-signal=SIGUSR2 --heapsnapshot-near-heap-limit=2 server.js
// Then kill -USR2 <pid> after warm-up (A) and again under load (B); compare in DevTools.
// A common retainer: a module-level map that only grows.
const reports = new Map<string, Report>();
export async function getReport(id: string) {
if (!reports.has(id)) reports.set(id, await buildReport(id)); // never evicted
return reports.get(id)!;
}
// Fix: bound it (max entries + TTL), or don't cache per-request data at module scope.CLOSE THE AI. EXPLAIN THIS.
What is the difference between raising --max-old-space-size and fixing a leak, and how do two heap snapshots tell you which object is retaining memory?RELATED SYMPTOMS
SOURCES
Guide reviewed