The Silent Killer in Our Node.js App
It started with sluggish performance. Our API response times crept up, and restarts temporarily fixed it—classic signs of a memory leak. Then, crash alerts hit:
"FATAL ERROR: JavaScript heap out of memory"
We discovered our Node.js process was leaking 2GB of RAM every 24 hours. Here’s how we found and fixed it—and how you can too.
1. Confirming the Leak
Symptoms:
✔ Rising heap usage (even when traffic was stable)
✔ Frequent GC cycles (garbage collector working overtime)
✔ Process crashes (heap out of memory
)
Tools to Detect Leaks:
-
process.memoryUsage()
(Basic tracking) -
--inspect
+ Chrome DevTools (Heap snapshots) -
clinic.js
(Automated leak detection)
// Log memory usage periodically
setInterval(() => {
const { heapUsed } = process.memoryUsage();
console.log(`Heap used: ${heapUsed / 1024 / 1024} MB`);
}, 5000);
🔍 Finding: Heap grew 50MB/hour—definitely a leak.
2. The Usual Suspects
A. Forgotten Timers & Intervals
// 🚨 Leak! Interval never cleared
setInterval(() => {
fetchData();
}, 1000);
// ✅ Fix: Always clear intervals
const interval = setInterval(fetchData, 1000);
clearInterval(interval); // When done
B. Stale Event Listeners
// 🚨 Leak! Listener keeps object in memory
socket.on('data', (data) => {
processData(data);
});
// ✅ Fix: Remove listeners
socket.off('data', processData);
C. Cached Objects Growing Indefinitely
// 🚨 Leak! Cache never purges
const cache = {};
app.get('/data/:id', (req, res) => {
if (!cache[req.params.id]) {
cache[req.params.id] = fetchExpensiveData();
}
res.json(cache[req.params.id]);
});
// ✅ Fix: Use WeakMap or LRU cache
const cache = new WeakMap(); // Auto-cleaned by GC
3. Advanced Debugging with Heap Snapshots
Step 1: Take a Snapshot
node --inspect app.js
- Open Chrome DevTools → Memory → Heap Snapshot
Step 2: Compare Snapshots
- Take Snapshot 1 (baseline)
- Take Snapshot 2 (after suspected leak)
-
Look for growing object counts (e.g.,
Array
,Closure
)
🔍 Our Culprit: A misconfigured logger retained request objects in memory.
4. Prevention Strategies
✔ Use --max-old-space-size
(Cap memory usage)
✔ Monitor with prom-client
(Grafana alerts)
✔ Adopt TypeScript (Catch leaks early with types)
Result: After fixes, memory stabilized at 200MB (from 2GB leaks).
Key Takeaways
🚨 Common leak sources: Timers, listeners, caches, closures
🔧 Tools: --inspect
, clinic.js
, heap snapshots
🛡️ Prevention: LRU caches, memory limits, monitoring
Ever chased a sneaky memory leak? Share your war story!
Top comments (0)