DEV Community

Alex Aslam
Alex Aslam

Posted on

Memory Leaks in Node.js: How We Hunted Down and Fixed a 2GB Leak in Production

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);
Enter fullscreen mode Exit fullscreen mode

🔍 Finding: Heap grew 50MB/hourdefinitely 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
Enter fullscreen mode Exit fullscreen mode

B. Stale Event Listeners

// 🚨 Leak! Listener keeps object in memory
socket.on('data', (data) => {
  processData(data);
});

// ✅ Fix: Remove listeners
socket.off('data', processData);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

3. Advanced Debugging with Heap Snapshots

Step 1: Take a Snapshot

node --inspect app.js
Enter fullscreen mode Exit fullscreen mode
  • 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!


Further Reading

Top comments (0)