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.jsIn Chrome DevTools (chrome://inspect) → Memory:
- Take Snapshot A (after warm-up).
- Wait 30 minutes under load → Take Snapshot B.
- 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.