Skip to content

How to Detect and Fix Memory Leaks in Node.js Applications

Software debugging on computer screen

When I deployed a Node.js API to production, memory usage grew slowly. After running for days, the process crashed with “out of memory” error. The culprit was a memory leak I hadn’t detected in testing.

Environment

  • Node.js v18+
  • Chrome DevTools
  • Production-like workload for testing

What happened?

My Express API processed user uploads. Memory started at 50MB but grew to 500MB over 24 hours. Eventually, the container got OOM-killed.

I checked the code but couldn’t find obvious leaks. The issue was hidden in closure patterns and uncleared caches.

How to solve it?

Step 1: Enable Node.js inspector

I started the application with inspector enabled:

Start with inspector
node --inspect app.js

For production debugging, I used a specific port:

Inspector on specific port
node --inspect=0.0.0.0:9229 app.js

Step 2: Take baseline heap snapshot

In Chrome, I opened chrome://inspect and clicked “inspect” on my Node.js process. Then:

  1. Opened Memory panel
  2. Selected “Heap snapshot”
  3. Clicked “Take snapshot” - named it “baseline”

This captured the initial memory state.

Step 3: Trigger suspected operations

I ran operations that might leak memory:

  • Processed 100 user uploads
  • Called API endpoints repeatedly
  • Triggered background tasks

Step 4: Take second snapshot and compare

After operations, I took another snapshot named “after-operations”. Then I selected “Comparison” view, comparing “after-operations” to “baseline”.

The comparison showed objects with growing count. I found:

  • 100 extra UploadHandler objects
  • Growing Buffer objects
  • Accumulating event listeners

Step 5: Trace retained objects

I clicked on a suspicious object type. DevTools showed the “Retainers” chain - what’s holding references to these objects.

This revealed the leak sources:

Common leak patterns I found

Pattern 1: Event listeners not removed

Leak: event listener retained
// BAD - listener keeps closure reference
function setupUploadHandler(upload) {
const handler = new UploadHandler(upload);
emitter.on('data', (chunk) => {
handler.process(chunk); // handler never released
});
}

The handler object stays alive because the event listener closure holds it.

Fix: remove listener on completion
// GOOD - cleanup listener
function setupUploadHandler(upload) {
const handler = new UploadHandler(upload);
const onData = (chunk) => handler.process(chunk);
emitter.on('data', onData);
handler.on('complete', () => {
emitter.removeListener('data', onData);
});
}

Pattern 2: Uncleared intervals

Leak: interval never cleared
// BAD - interval runs forever
function startPolling() {
setInterval(() => {
fetchStatus(); // interval reference retained
}, 1000);
}
Fix: clear interval on cleanup
// GOOD - store and clear interval
let intervalId;
function startPolling() {
intervalId = setInterval(fetchStatus, 1000);
}
function stopPolling() {
clearInterval(intervalId);
}

Pattern 3: Unbounded cache

Leak: cache grows forever
// BAD - unlimited cache
const cache = {};
function addToCache(key, value) {
cache[key] = value; // never cleared
}
Fix: limit cache size
// GOOD - bounded cache with limit
const cache = new Map();
const MAX_CACHE_SIZE = 1000;
function addToCache(key, value) {
if (cache.size >= MAX_CACHE_SIZE) {
// Remove oldest entry (simple LRU)
const firstKey = cache.keys().next().value;
cache.delete(firstKey);
}
cache.set(key, value);
}

Or use WeakMap for objects that can be garbage collected:

Fix: WeakMap for object keys
// GOOD - WeakMap allows GC of keys
const cache = new WeakMap();
function addToCache(objKey, value) {
cache.set(objKey, value);
// When objKey is GC'd, entry removed automatically
}

The reason

I think memory leaks happen when:

  1. Unexpected references - Closure captures variables, event listeners hold references, caches accumulate.

  2. No cleanup logic - Intervals, timeouts, listeners created but never removed.

  3. V8 GC can’t help - Garbage collector only removes truly unreachable objects. Leaked objects are reachable, so GC keeps them.

Summary

In this post, I showed how to detect and fix Node.js memory leaks. The key point is using Chrome DevTools heap snapshot comparison to find retained objects. Fix leaks by removing event listeners, clearing intervals, and limiting cache sizes. Memory leak prevention is essential for stable production deployments.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments