JS Runtimes

Advanced Memory Management in Node.js: Garbage Collection and Heap Profiling

Master Node.js memory mechanics. Learn about V8 heap spaces (New Space, Old Space), garbage collection algorithms, and how to analyze heap snapshots.

Sachin Sharma
Sachin SharmaCreator
Jun 1, 2026
5 min read
Advanced Memory Management in Node.js: Garbage Collection and Heap Profiling
Featured Resource
Quick Overview

Master Node.js memory mechanics. Learn about V8 heap spaces (New Space, Old Space), garbage collection algorithms, and how to analyze heap snapshots.

Advanced Memory Management in Node.js: Garbage Collection and Heap Profiling

In small-scale applications, memory leaks in Node.js often go unnoticed. If a process leaks a few kilobytes per hour, a daily container restart will mask the issue.

However, in high-concurrency enterprise microservices, memory leaks are catastrophic. They trigger severe garbage collection pauses (increasing p99 latency), throttle CPU cycles, and eventually trigger the dreaded operating system crash:

FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory

To build truly resilient Node.js services, you must master V8's memory model, understand how garbage collection spaces operate, and know how to programmatically profile the heap under load.


⚑ 1. The V8 Memory Model: Heap vs Stack

V8 divides memory allocations into two primary segments:

A. The Stack

Stores primitive values (numbers, booleans, strings) and reference pointers to objects stored on the heap. Stack frames are managed directly by the CPU; when a function finishes executing, its stack memory is popped and freed immediately.

B. The Heap

Stores reference types (objects, arrays, functions, closures). Because these have dynamic sizes and lifetimes, they cannot be managed on the stack. The heap is where Garbage Collection (GC) takes place.

V8 splits the heap into distinct Memory Spaces:

  • New Space (Young Generation): A small space (typically 16MB to 64MB) where newly created objects are allocated. V8 runs a very fast GC pass here (Scavenge) frequently.
  • Old Space (Old Generation): Objects that survive multiple Scavenge passes are promoted here. Old Space is split into Old Pointer Space (objects containing references to other objects) and Old Data Space (raw data like strings or buffers). V8 runs a heavy GC pass here (Mark-Sweep-Compact) when memory limits are reached.
  • Large Object Space: For objects larger than the size limits of other spaces. V8 bypasses garbage collection movements here entirely.
  • Code Space: Where V8's JIT compiler stores compiled machine code blocks.

πŸ—οΈ 2. How V8 Garbage Collection Works

V8 uses a generational garbage collection strategy. Because most objects die shortly after allocation (high mortality rate), separating new and old objects maximizes efficiency.

Phase 1: Scavenger GC (New Space)

New Space is divided into two equal halves: From Space and To Space.

  1. 2.
    All new allocations go into the From Space.
  2. 4.
    When it fills up, V8 runs a Scavenge pass.
  3. 6.
    V8 traverses active references. Alive objects are copied directly into the To Space (compacting them to prevent fragmentation).
  4. 8.
    Dead objects are discarded.
  5. 10.
    The From and To spaces swap roles. If an object survives a second Scavenge pass, it is promoted directly to the Old Space.

Phase 2: Mark-Sweep-Compact GC (Old Space)

When the Old Space reaches its heap limit, V8 executes a full GC:

  1. 2.
    Marking: V8 traverses the reference graph starting from "Roots" (global variables, current stack frames). It marks all reachable objects as alive.
  2. 4.
    Sweeping: V8 walks the memory addresses and adds the memory of unmarked (dead) objects to "free lists" so new allocations can use them.
  3. 6.
    Compacting: To prevent memory fragmentation (where free blocks are scattered, making it impossible to allocate a large contiguous object), V8 shifts live objects together, updating all reference pointers.

πŸ’» 3. Identifying Memory Leaks with Heap Profiling

Let's write a simple script that programmatically takes a V8 Heap Snapshot when memory consumption crosses a warning threshold.

First, write a utility using the native v8 module:

javascript
import fs from 'node:fs'; import v8 from 'node:v8'; import process from 'node:process'; function inspectMemoryUsage() { const memory = process.memoryUsage(); const heapUsedMB = (memory.heapUsed / 1024 / 1024).toFixed(2); const rssMB = (memory.rss / 1024 / 1024).toFixed(2); console.log(`πŸ“ˆ Memory Monitor | Heap Used: \${heapUsedMB} MB | RSS: \${rssMB} MB`); // If heap usage exceeds 85% of allocated memory, trigger snapshot const heapLimit = v8.getHeapStatistics().heap_size_limit; const currentRatio = memory.heapUsed / heapLimit; if (currentRatio > 0.85) { console.warn("⚠️ High memory usage detected! Programmatically generating heap snapshot..."); takeHeapSnapshot(); } } function takeHeapSnapshot() { const snapshotStream = v8.getHeapSnapshot(); const fileName = `snapshot-\${Date.now()}.heapsnapshot`; const fileStream = fs.createWriteStream(fileName); snapshotStream.pipe(fileStream); fileStream.on('finish', () => { console.log(`πŸ’Ύ Successfully saved heap snapshot: \${fileName}`); }); } // Check memory every 10 seconds setInterval(inspectMemoryUsage, 10000);

πŸ› οΈ 4. Common Causes of Leaks in Node.js

  1. 2.
    Accidental Global Variables: Declaring variables without const, let, or var attaches them directly to the global context, preventing them from ever being garbage collected.
  2. 4.
    Closures holding old scopes: If an inner function is retained in memory (e.g., in an event listener), it retains references to the entire lexical scope environment it was created in.
  3. 6.
    Cached data without eviction strategies: Storing user sessions or items in a raw JavaScript object (const cache = {}) without a Max-Age or Size limit (like an LRU Cache) will inevitably consume all heap memory under continuous traffic.

🏁 5. Conclusion

Mastering Node.js memory profiling transitions you from reacting to arbitrary Out-of-Memory crashes to proactively designing zero-leak, highly performant systems. By setting up automated heap snapshotting triggers, you can confidently run high-concurrency microservices at peak efficiency.

Sachin Sharma

Sachin Sharma

Software Developer

Building digital experiences at the intersection of design and code. Sharing weekly insights on engineering, productivity, and the future of tech.