How to Detect and Fix Memory Leaks in Node.js Applications
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:
node --inspect app.jsFor production debugging, I used a specific port:
node --inspect=0.0.0.0:9229 app.jsStep 2: Take baseline heap snapshot
In Chrome, I opened chrome://inspect and clicked “inspect” on my Node.js process. Then:
- Opened Memory panel
- Selected “Heap snapshot”
- 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
UploadHandlerobjects - Growing
Bufferobjects - 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
// BAD - listener keeps closure referencefunction 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.
// GOOD - cleanup listenerfunction 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
// BAD - interval runs foreverfunction startPolling() { setInterval(() => { fetchStatus(); // interval reference retained }, 1000);}// GOOD - store and clear intervallet intervalId;function startPolling() { intervalId = setInterval(fetchStatus, 1000);}function stopPolling() { clearInterval(intervalId);}Pattern 3: Unbounded cache
// BAD - unlimited cacheconst cache = {};function addToCache(key, value) { cache[key] = value; // never cleared}// GOOD - bounded cache with limitconst 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:
// GOOD - WeakMap allows GC of keysconst 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:
-
Unexpected references - Closure captures variables, event listeners hold references, caches accumulate.
-
No cleanup logic - Intervals, timeouts, listeners created but never removed.
-
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