Docs / Troubleshooting / Debug Memory Leaks in Node.js Applications

Debug Memory Leaks in Node.js Applications

By Admin · Mar 15, 2026 · Updated Apr 23, 2026 · 408 views · 4 min read

Node.js memory leaks cause your application's memory usage to grow continuously until it crashes with an out-of-memory error or gets killed by the OOM killer. This guide covers detecting, diagnosing, and fixing memory leaks in production Node.js applications on your VPS.

Detect Memory Leaks

# Monitor Node.js memory from outside
watch -n 5 "ps -o pid,rss,vsz,command -p $(pgrep -f 'node')"

# Check if memory grows over time
while true; do
    rss=$(ps -o rss= -p $(pgrep -f 'node app.js'))
    echo "$(date '+%H:%M:%S') RSS: ${rss}KB"
    sleep 60
done | tee /tmp/node-memory.log

# Inside your app, track memory
setInterval(() => {
    const mem = process.memoryUsage();
    console.log(JSON.stringify({
        timestamp: new Date().toISOString(),
        rss: Math.round(mem.rss / 1024 / 1024),
        heapUsed: Math.round(mem.heapUsed / 1024 / 1024),
        heapTotal: Math.round(mem.heapTotal / 1024 / 1024),
        external: Math.round(mem.external / 1024 / 1024),
    }));
}, 30000);  // Every 30 seconds

Common Causes

  • Growing arrays/maps: Caches without eviction, event listeners accumulating
  • Closures: Variables captured in closures that prevent garbage collection
  • Event listeners: Adding listeners without removing them (EventEmitter leak warning)
  • Global variables: Data stored globally that grows over time
  • Uncleared timers: setInterval/setTimeout references preventing GC
  • Stream backpressure: Readable streams producing faster than writable consumes

Take Heap Snapshots

// Add to your app for on-demand snapshots
const v8 = require('v8');
const fs = require('fs');

function takeHeapSnapshot() {
    const filename = `/tmp/heap-${Date.now()}.heapsnapshot`;
    const snapshotStream = v8.writeHeapSnapshot(filename);
    console.log(`Heap snapshot written to ${filename}`);
    return filename;
}

// Trigger via signal
process.on('SIGUSR2', () => {
    takeHeapSnapshot();
});

// Or via HTTP endpoint (development only!)
app.get('/debug/heap', (req, res) => {
    const file = takeHeapSnapshot();
    res.json({ snapshot: file });
});

// Take snapshots at intervals to compare
// kill -USR2 
// Download the .heapsnapshot file
// Open in Chrome DevTools > Memory tab

Analyze with Chrome DevTools

# Start Node.js with inspector
node --inspect app.js

# Or attach to running process
kill -USR1 $(pgrep -f 'node app.js')
# This enables the inspector on port 9229

# SSH tunnel for remote debugging
ssh -L 9229:localhost:9229 user@your-vps

# Open chrome://inspect in Chrome
# 1. Take Heap Snapshot #1
# 2. Wait/exercise the app
# 3. Take Heap Snapshot #2
# 4. Compare: select snapshot #2, switch to "Comparison" view
# 5. Sort by "# Delta" to find growing objects

Fix Common Leaks

// LEAK: Growing cache without limits
const cache = {};
function getUser(id) {
    if (!cache[id]) cache[id] = fetchUser(id);  // Never evicted!
    return cache[id];
}

// FIX: Use LRU cache with size limit
const LRU = require('lru-cache');
const cache = new LRU({ max: 500, ttl: 1000 * 60 * 5 });

// LEAK: Event listeners not removed
socket.on('data', handleData);
// If socket is recreated but handlers aren't removed...

// FIX: Clean up listeners
socket.on('data', handleData);
socket.on('close', () => {
    socket.removeListener('data', handleData);
});

// LEAK: Closures holding references
function processLargeData(data) {
    const bigBuffer = Buffer.alloc(100 * 1024 * 1024);
    return function callback() {
        // bigBuffer is captured in closure even if not used!
        return data.length;
    };
}

// FIX: Don't capture unnecessary variables
function processLargeData(data) {
    const length = data.length;
    return function callback() {
        return length;  // Only captures the primitive
    };
}

// LEAK: Uncleared intervals
const interval = setInterval(checkHealth, 5000);
// If the module is replaced/reloaded, interval keeps running

// FIX: Clear on shutdown
process.on('SIGTERM', () => {
    clearInterval(interval);
    process.exit(0);
});

Production Monitoring

# Use --max-old-space-size to set memory limit
node --max-old-space-size=1024 app.js  # 1GB limit

# Enable GC logging
node --trace-gc app.js 2>&1 | grep "Mark-sweep"
# Watch for growing heap after GC

# Automatic heap dump on OOM
node --heapsnapshot-signal=SIGUSR2 --max-old-space-size=1024 app.js

# Use clinic.js for production profiling
npx clinic doctor -- node app.js
npx clinic heapprofiler -- node app.js

Best Practices

  • Monitor heap usage with process.memoryUsage() and alert on continuous growth
  • Set --max-old-space-size to prevent the app from consuming all server memory
  • Use bounded caches: Always set size limits on in-memory caches
  • Remove event listeners when they're no longer needed
  • Take comparison heap snapshots to identify what's growing between snapshots
  • Profile regularly: Run clinic.js or heapprofiler as part of your testing

Was this article helpful?