Memory Management
Node.js runs on the V8 JavaScript engine, which manages memory automatically through a generational garbage collector. Understanding how V8 organizes memory helps you write code that puts less pressure on the GC, diagnose memory leaks, and avoid the class of bugs — event listener accumulation, closure captures, forgotten caches — that cause long-running Node processes to bloat and eventually crash. This page covers V8's heap spaces, the stack vs. heap distinction, how Buffer memory works outside the V8 heap, and how to measure and diagnose memory usage.
V8 heap spaces
V8 Memory Layout
├─ Stack Function call frames, local primitives (number, boolean, pointer)
│ Fast allocation/deallocation; limited size (~1MB default)
│
└─ Heap
├─ New Space (Young Generation) ~1–8 MB
│ Objects allocated here first; short-lived objects die here (minor GC)
│ Two semi-spaces (from/to): GC copies survivors, frees the other half
│
├─ Old Space (Old Generation) most of the heap
│ Objects that survive 2 minor GCs are promoted here
│ Major GC (mark-sweep-compact) runs here — more expensive
│
├─ Code Space Compiled JIT code
├─ Map Space Hidden class descriptors (object shapes)
└─ Large Object Space Objects > ~500KB; never moved by GC
Outside V8 heap (not counted in heap stats):
Buffer / TypedArray Node's Buffer allocates via malloc, tracked separatelyChecking memory usage at runtime
// process.memoryUsage() — snapshot of current memory state:
const mem = process.memoryUsage()
console.log({
heapTotal: Math.round(mem.heapTotal / 1024 / 1024) + ' MB', // V8 heap capacity
heapUsed: Math.round(mem.heapUsed / 1024 / 1024) + ' MB', // live objects
rss: Math.round(mem.rss / 1024 / 1024) + ' MB', // total process memory (heap + stack + buffers)
external: Math.round(mem.external / 1024 / 1024) + ' MB', // C++ objects (Buffers)
arrayBuffers: Math.round(mem.arrayBuffers / 1024 / 1024) + ' MB', // SharedArrayBuffer + ArrayBuffer
}){
heapTotal: 48 MB,
heapUsed: 32 MB,
rss: 75 MB,
external: 2 MB,
arrayBuffers: 0 MB
}Buffer memory — outside the V8 heap
// Node.js Buffer allocates memory via malloc (C++ layer), NOT in the V8 heap.
// This memory is tracked in process.memoryUsage().external, not heapUsed.
// Buffer.alloc — zero-fills for security (slower, use for sensitive data):
const secure = Buffer.alloc(1024)
// Buffer.allocUnsafe — skips zero-fill, may contain stale memory (faster):
const fast = Buffer.allocUnsafe(1024)
// Buffer.from(string) — copies string data into a new Buffer:
const buf = Buffer.from('hello world', 'utf8')
// ⚠️ Buffer.allocUnsafe content is UNINITIALIZED — always write before reading:
const b = Buffer.allocUnsafe(16)
b.fill(0) // or write your data before exposing it to user code
// Never send Buffer.allocUnsafe content to a client without writing to it first.Common memory leak patterns
Pattern | Why it leaks | Fix |
|---|---|---|
Accumulated event listeners |
| Call |
Global/module-level caches | Map/array appended to on every request but never evicted | Use LRU cache with size/TTL limit (e.g. |
Closures capturing large objects | Inner function holds reference to outer scope variable — prevents GC even after outer function returns | Null out large variables when done; avoid capturing entire |
Forgotten timers/intervals |
| Store the ID and call |
Unended streams | Readable/Writable stream not consumed or destroyed — buffers back-pressure in memory | Always pipe to a destination or call |
Diagnosing memory leaks with heap snapshots
import v8 from 'node:v8'
import fs from 'node:fs'
// Write a heap snapshot to disk — open in Chrome DevTools (Memory tab → Load profile):
function writeHeapSnapshot(label: string) {
const filename = `heap-${label}-${Date.now()}.heapsnapshot`
const snapshot = v8.writeHeapSnapshot(filename)
console.log('Heap snapshot written:', snapshot)
}
// Typical workflow:
// 1. writeHeapSnapshot('baseline') — before load
// 2. Run load / exercise the leak path
// 3. writeHeapSnapshot('after') — after load
// 4. Compare in Chrome DevTools: filter by "Objects allocated between snapshots"
// to see what grew — the retainer chain shows what's holding the referenceSetting the heap size limit
# Default heap limit is ~1.5 GB on 64-bit (V8 sets this based on available RAM) # Increase for memory-intensive workloads: node --max-old-space-size=4096 server.js # 4 GB old space # Or via NODE_OPTIONS environment variable: NODE_OPTIONS="--max-old-space-size=4096" node server.js # ⚠️ This is a ceiling, not a target — the process still uses only what it needs. # If your process regularly hits the limit, you have a leak or need to redesign.