Mastering Node.js Garbage Collection: Detect and Prevent Memory Leaks

This article explains how Node.js relies on the V8 engine for automatic memory management, details the garbage‑collection mechanisms (Scavenge, Mark‑Sweep, Mark‑Compact), shows practical code for monitoring and forcing GC, illustrates common memory‑leak patterns, and recommends tools for diagnosing and fixing leaks.

Node Underground
Node Underground
Node Underground
Mastering Node.js Garbage Collection: Detect and Prevent Memory Leaks

Quick Navigation

GC in Node.js

GC practice and memory‑management

V8 GC mechanism

Memory leaks

Memory‑inspection tools

GC in Node.js

Node.js runs on the Chrome V8 engine, so its garbage collection is essentially V8's GC. Like Java, memory is managed automatically by the virtual machine.

However, developers still need to understand how the VM uses memory because programming mistakes can cause serious memory leaks.

GC Practice in Node.js

Let's see how garbage collection works in Node.js with a demo.

Leak Detection

Node.js provides process.memoryUsage() to inspect current memory usage (bytes):

rss : resident set size – total memory occupied by the process (code, stack, heap).

heapTotal : total heap memory allocated.

heapUsed : heap memory currently used (primary metric for leak detection).

external : memory used by V8 internal C++ objects.

/**
 * Format bytes to MB string
 */
const format = function (bytes) {
  return (bytes / 1024 / 1024).toFixed(2) + ' MB';
};

/**
 * Print memory usage
 */
const print = function () {
  const memoryUsage = process.memoryUsage();
  console.log(JSON.stringify({
    rss: format(memoryUsage.rss),
    heapTotal: format(memoryUsage.heapTotal),
    heapUsed: format(memoryUsage.heapUsed),
    external: format(memoryUsage.external)
  }));
};

Leak Example

The following code creates a Fruit object stored on the heap. The banana instance allocates a huge array, causing a large increase in heapUsed.

// example.js
function Quantity(num) {
  if (num) {
    return new Array(num * 1024 * 1024);
  }
  return num;
}

function Fruit(name, quantity) {
  this.name = name;
  this.quantity = new Quantity(quantity);
}

let apple = new Fruit('apple');
print();
let banana = new Fruit('banana', 20);
print();

Running the script shows apple using only ~4.21 MB, while banana pushes heapUsed to ~164 MB.

{"rss":"19.94 MB","heapTotal":"6.83 MB","heapUsed":"4.21 MB","external":"0.01 MB"}
{"rss":"180.04 MB","heapTotal":"166.84 MB","heapUsed":"164.24 MB","external":"0.01 MB"}

Because the root set still references banana, the memory cannot be reclaimed until the next GC cycle.

Manual GC

Assigning banana = null and invoking global.gc() (enabled with --expose-gc) releases the memory, reducing heapUsed from 164 MB to ~3.97 MB.

$ node --expose-gc example.js
{"rss":"19.95 MB","heapTotal":"6.83 MB","heapUsed":"4.21 MB","external":"0.01 MB"}
{"rss":"180.05 MB","heapTotal":"166.84 MB","heapUsed":"164.24 MB","external":"0.01 MB"}
{"rss":"52.48 MB","heapTotal":"9.33 MB","heapUsed":"3.97 MB","external":"0.01 MB"}

After GC, the banana node disappears from the heap diagram.

V8 Garbage‑Collection Mechanism

GC reclaims objects that are no longer reachable from the root set (global objects, local variables, etc.).

V8 Heap Size Limits

On 64‑bit machines V8 caps the heap at ~1.4 GB; on 32‑bit machines at ~0.7 GB. Exceeding these limits causes the process to exit.

Overflow example

// overflow.js
const format = function (bytes) {
  return (bytes / 1024 / 1024).toFixed(2) + ' MB';
};

const print = function () {
  const memoryUsage = process.memoryUsage();
  console.log(`heapTotal: ${format(memoryUsage.heapTotal)}, heapUsed: ${format(memoryUsage.heapUsed)}`);
};

const total = [];
setInterval(function () {
  total.push(new Array(20 * 1024 * 1024)); // large allocation
  print();
}, 1000);

Each interval adds ~160 MB to the heap; once the limit is reached, allocation fails and the process crashes.

Young and Old Generations

Most objects die quickly (young generation, ~1‑8 MB). V8 uses the Scavenge algorithm (a copying collector) for fast reclamation. Surviving objects are promoted to the old generation, which uses Mark‑Sweep and Mark‑Compact algorithms.

Young Generation (Scavenge)

Scavenge splits the space into two equal halves (from‑space and to‑space). Live objects are copied to to‑space (or promoted), and the spaces are swapped.

Old Generation

Objects that survive multiple collections are kept here. Mark‑Sweep first marks live objects, then clears dead ones. This can cause fragmentation, which Mark‑Compact resolves by moving live objects together.

V8 GC Summary

V8 employs Scavenge for the young generation, Mark‑Sweep and Mark‑Compact for the old generation, and incremental marking to reduce pause times. GC pauses affect the JavaScript thread, especially when many objects survive in the old space.

Memory Leaks

A memory leak occurs when allocated heap memory cannot be released, leading to performance degradation or crashes.

Global Variables

Undeclared variables or properties attached to global persist for the lifetime of the process unless explicitly deleted or set to null / undefined.

Closures

Closures retain references to variables in their outer scope. If a closure is never released, the captured variables also stay in memory, causing leaks.

Example from Meteor:

var theThing = null;
var replaceThing = function () {
  var originalThing = theThing;
  var unused = function () {
    if (originalThing) {
      console.log("hi");
    }
  };
  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () { console.log(someMessage); }
  };
};
setInterval(replaceThing, 1000);

Each call creates a new object while the previous one remains referenced via the closure chain.

Cache Misuse

Storing large amounts of data in memory (e.g., a Map of user tokens) can cause leaks, especially in multi‑process deployments where each process holds its own copy.

const memoryStore = new Map();
exports.getUserToken = function (key) {
  const token = memoryStore.get(key);
  if (token && Date.now() - token.now > 2 * 60) {
    return token;
  }
  const dbToken = db.get(key);
  memoryStore.set(key, { now: Date.now(), val: dbToken });
  return token;
};

Module Private Variables

Node wraps each module in a function, creating a closure that keeps module‑level variables alive for the process lifetime. Load modules once and cache the exported object.

Repeated Event Listeners

Adding too many listeners to an EventEmitter triggers a MaxListenersExceededWarning, indicating a potential leak. Use emitter.setMaxListeners() judiciously.

(node:23992) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 connect listeners added. Use emitter.setMaxListeners() to increase limit

Other Tips

Always clear intervals with clearInterval, avoid unnecessary array creations (prefer forEach over map when not needed), and be mindful of functions that allocate large temporary structures.

console.log(setInterval(function(){}, 1000)); // returns an id
[1,2,3].filter(item => item % 2 === 0); // creates a new array
[1,2,3].map(item => item % 2 === 0); // creates a new array of booleans

Memory‑Inspection Tools

node-heapdump – dumps V8 heap snapshots.

node-profiler – Alinode’s tool for capturing heap snapshots.

Easy-Monitor – lightweight performance monitoring for Node.js.

Node.js‑Troubleshooting‑Guide – comprehensive guide for diagnosing and optimizing Node.js applications.

alinode – performance platform offering monitoring, security alerts, and optimization services.

Further Reading

Node.js Garbage Collection Explained

A tour of V8: Garbage Collection (Chinese)

Memory Management Reference

深入浅出 Node.js

如何分析 Node.js 中的内存泄漏

公众号 “Nodejs技术栈”

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Node.jsgarbage collectionMemory Leakv8
Node Underground
Written by

Node Underground

No language is immortal—Node.js isn’t either—but thoughtful reflection is priceless. This underground community for Node.js enthusiasts was started by Taobao’s Front‑End Team (FED) to share our original insights and viewpoints from working with Node.js. Follow us. BTW, we’re hiring.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.