Debugging Memory Leaks in Node.js — A Practical Walkthrough

A Node.js process that slowly eats more and more memory until it gets OOM-killed is one of the more frustrating production issues to chase down, because the symptom (crash) shows up long after the actual cause. This guide walks through how to detect a leak, capture heap snapshots, and fix the most common root causes.

Recognizing the Symptom

<--- Last few GCs --->
[12345:0x...] 240012 ms: Mark-sweep 1998.4 (2050.1) -> 1997.9 (2051.3) MB

<--- JS stacktrace --->
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
1: 0xb01010 node::Abort()
2: 0xa1f6a5 node::FatalError()

Before jumping to heap snapshots, confirm it's actually a leak rather than a single large allocation. Log heap usage over time under normal load:

setInterval(() => {
  const { heapUsed, heapTotal, rss } = process.memoryUsage();
  console.log({
    heapUsedMB: (heapUsed / 1024 / 1024).toFixed(1),
    heapTotalMB: (heapTotal / 1024 / 1024).toFixed(1),
    rssMB: (rss / 1024 / 1024).toFixed(1),
  });
}, 30_000);

If heapUsed keeps climbing and never comes back down between garbage collection cycles, even when traffic is idle, that's a real leak.

Capturing Heap Snapshots with --inspect

  1. Start your app with the inspector flag: node --inspect server.js
  2. Open Chrome and navigate to chrome://inspect
  3. Click "inspect" under Remote Target to open Chrome DevTools connected to your Node process
  4. Go to the Memory tab, select "Heap snapshot", and take a snapshot
  5. Generate load / wait a few minutes, then take a second snapshot
  6. Use the "Comparison" view between the two snapshots to see which object types grew
# For a running production process, send SIGUSR2 (with heapdump module)
# or use --inspect on a staging replica under synthetic load — avoid
# attaching --inspect directly to a live production process if possible.
node --inspect=0.0.0.0:9229 server.js

Common Leak Source: Event Listeners

Adding a listener on every request without ever removing it is one of the most common leaks in Node.js servers.

// Leaky: a new listener is added on every request and never removed
app.get('/stream', (req, res) => {
  eventEmitter.on('update', (data) => res.write(data)); // never off()'d
});

// Fixed: remove the listener when the request/connection ends
app.get('/stream', (req, res) => {
  const onUpdate = (data) => res.write(data);
  eventEmitter.on('update', onUpdate);

  req.on('close', () => {
    eventEmitter.off('update', onUpdate);
  });
});

Node also warns you about this directly: MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 update listeners added. Treat that warning as a real bug report, not noise to silence.

Common Leak Source: Closures and Unbounded Caches

// Leaky: cache grows forever, nothing is ever evicted
const cache = new Map();
function getUser(id) {
  if (!cache.has(id)) cache.set(id, fetchUserFromDb(id));
  return cache.get(id);
}

// Fixed: bound the cache size, e.g. with an LRU cache
const LRU = require('lru-cache');
const cache = new LRU({ max: 500, ttl: 1000 * 60 * 10 });

Closures that capture large objects (like an entire request or response object) inside a long-lived callback — a timer, an event listener, or a promise stored somewhere global — also keep those objects alive far longer than intended.

Prevention Checklist

  • Always pair .on() with a matching .off()/.removeListener() when the subscriber's lifetime ends
  • Bound any in-memory cache with a max size and TTL — never let a Map or array grow without limit
  • Clear setInterval/setTimeout timers when they're no longer needed
  • Watch for MaxListenersExceededWarning in logs and investigate immediately, don't just raise the limit
  • Add heap usage metrics to your monitoring dashboard so leaks are caught before they cause an outage

Frequently Asked Questions

How do I know if my Node.js app has a memory leak?

Watch process.memoryUsage().heapUsed over time under steady load. If it climbs continuously and never drops back down after garbage collection, even during idle periods, you likely have a memory leak.

What is the most common cause of memory leaks in Node.js?

Event listeners that are added but never removed, and global caches or arrays that grow indefinitely without eviction, are the two most common causes of memory leaks in long-running Node.js applications.

How do I take a heap snapshot in Node.js?

Run your app with node --inspect server.js, open chrome://inspect in Chrome, click "inspect" on your process, and use the Memory tab to take heap snapshots. Comparing two snapshots taken minutes apart highlights objects that keep growing.

Debug Errors Faster with AI

Got a confusing "heap out of memory" crash or an unfamiliar V8 error trace? Paste it into our free AI Error Explainer for a plain-English cause and fix.

Related articles