Skip to main content
Back to homepage
Diagnosis & Repair Guide

Node.js Memory Leak — Diagnosis & Fix Guide

Practical guide for diagnosing and fixing heap memory leaks in Node.js applications. Covers common causes, tools, and step-by-step debugging workflow.

Memory leaks in Node.js applications are among the most critical production issues. This guide outlines the end-to-end workflow — from initial detection to a confirmed fix.


🔍 Is It Really a Leak?

A memory leak is defined as a sustained, unbounded increase in heap usage.

Behaviors That Look Like Leaks But Are Normal:

  • Warm-up Growth: Heap climbs for the first few minutes after boot, then plateaus (Normal JIT compilation & module load).
  • In-Memory Caches: Intentional growth as items populate an unbounded cache (Expected behavior).
  • Garbage Collection (GC) Sawtooth: Heap climbs between GC cycles, then drops sharply after collection (Healthy).

Important

A true memory leak continuously climbs for hours under steady load without plateauing.


🛑 4 Most Common Causes

1. Retained EventEmitter Listeners

// ❌ LEAKS: 'finish' listener holds a reference to `context`
server.on('connection', (socket) => {
  const context = buildContext(socket);
  socket.on('data', (chunk) => process(chunk, context));
  // Missing: socket.on('close', () => cleanup())
});

Fix: Always unbind listeners when the target object is destroyed or closed.


2. Unbounded Maps or Sets Used as Caches

// ❌ LEAKS: Cache grows infinitely without eviction
const cache = new Map();
 
app.get('/item/:id', async (req, res) => {
  if (!cache.has(req.params.id)) {
    cache.set(req.params.id, await db.getItem(req.params.id));
  }
  res.json(cache.get(req.params.id));
});

Fix: Use a size-bounded LRU cache (lru-cache package) with max limits and TTL.


3. Timers (setInterval) Holding Object References

// ❌ LEAKS: Interval runs indefinitely, keeping references alive
function startPoller(callback) {
  const id = setInterval(callback, 1000);
}

Fix: Always return timer IDs and call clearInterval(id) when teardown occurs.


4. Closures Capturing Large Buffers

// ❌ LEAKS: Retains full 100 MB buffer in memory
function processLargeFile(buffer) {
  return {
    getFirst: () => buffer[0], // Closure captures full 100 MB buffer reference
  };
}

Fix: Extract only primitive values from large objects before forming closures.


🛠️ Step-by-Step Diagnostic Workflow

Step 1 — Confirm the Heap Growth Trend

Add memory logging to your application:

setInterval(() => {
  const m = process.memoryUsage();
  console.log(JSON.stringify({
    heapUsed: Math.round(m.heapUsed / 1024 / 1024) + ' MB',
    heapTotal: Math.round(m.heapTotal / 1024 / 1024) + ' MB',
    external: Math.round(m.external / 1024 / 1024) + ' MB',
  }));
}, 60_000);

Step 2 — Take Three Heap Snapshots

Launch Node with inspection enabled:

node --inspect src/server.js

In Chrome DevTools (chrome://inspect) → Memory:

  1. Take Snapshot A (after warm-up).
  2. Wait 30 minutes under load → Take Snapshot B.
  3. Wait another 30 minutes under load → Take Snapshot C.

Step 3 — Isolate Accumulated Objects

In Snapshot C, filter by "Objects allocated between Snapshot A and B". Sort by Retained Size.


Step 4 — Trace the Retainer Chain

Inspect the leaked object in the Retainers panel to identify which parent reference prevents Garbage Collection.


🧰 Tooling Reference

Tool Primary Use Case
Chrome DevTools → Memory Heap snapshot comparison & retainer inspection
node --heap-snapshot-signal=SIGUSR2 Trigger snapshots in production without restarts
clinic.js Automated flamecharts & profiling
lru-cache Memory-safe bounded cache replacement

Note

In production, trigger snapshots without restarting the process: kill -USR2 <pid>.


Still stuck? Describe your memory leak and I will estimate how long it would take to diagnose and fix.

Frequently Asked Questions

How do I know if my Node.js app is leaking memory?

Monitor process.memoryUsage().heapUsed over time. A healthy app stays roughly flat after warm-up. If it climbs continuously for more than 30 minutes under normal load, you likely have a leak.

What is the fastest way to take a heap snapshot in Node.js?

Start your app with node --inspect then open chrome://inspect in Chrome. Click 'inspect', go to the Memory tab, and click 'Take snapshot'.

Are EventEmitters a common source of leaks?

Yes — they are one of the most frequent causes. If you add a listener to a long-lived object (like a server, stream, or response) and never remove it, both the listener and any variables captured in its closure will be retained.

Can I fix a memory leak without taking heap snapshots?

Sometimes — if you know common patterns (EventEmitter accumulation, unclosed streams, growing caches). But blind guessing wastes time. Heap snapshots reduce investigation from hours to minutes.

Experiencing a similar issue?

Run a diagnostic report with our AI tools to receive a scoping estimate and booking details in seconds.

Run diagnosis