Skip to main content

Node.js Memory Leak Detector

Diagnose and fix Node.js memory leaks using heap snapshots, clinic.js, and process.memoryUsage(). Covers the most common leak patterns and production monitoring strategies.

✓ Heap snapshot analysis ✓ clinic.js diagnosis ✓ Common leak patterns ✓ Production monitoring

Quick Memory Health Check

// memory-monitor.ts — add to any Node.js app
function logMemory(label = '') {
  const m = process.memoryUsage();
  console.log(`[Memory${label ? ' ' + label : ''}]`, {
    heapUsed:  `${(m.heapUsed  / 1024 / 1024).toFixed(1)} MB`,
    heapTotal: `${(m.heapTotal / 1024 / 1024).toFixed(1)} MB`,
    rss:       `${(m.rss       / 1024 / 1024).toFixed(1)} MB`,
    external:  `${(m.external  / 1024 / 1024).toFixed(1)} MB`,
  });
}

// Log every 60 seconds — if heapUsed grows continuously: leak suspected
setInterval(() => logMemory('60s'), 60_000).unref();

// Or use with load test:
logMemory('before');
await runLoadTest();
global.gc?.();  // force GC (start with --expose-gc flag)
logMemory('after');

Heap Snapshot with Chrome DevTools

# 1. Start app with inspector
node --inspect server.js
# or attach inspector to running process:
node --inspect=0.0.0.0:9229 server.js

# 2. Open Chrome → chrome://inspect → click your target
# 3. Memory tab → Heap snapshot → Take snapshot (before)
# 4. Perform the leaking action 10-20 times
# 5. Take snapshot (after)
# 6. Switch dropdown to "Comparison" mode
# 7. Sort by "# Delta" — growing object counts indicate the leak
// Programmatic heap snapshot (useful in staging)
const v8 = require('v8');
const path = require('path');

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

// Trigger on SIGUSR2 (kill -USR2 <pid>)
process.on('SIGUSR2', () => takeHeapSnapshot());

clinic.js — Automated Diagnosis

# Install clinic.js
npm install -g clinic autocannon

# 1. Run Doctor (auto-diagnoses memory, CPU, event loop)
clinic doctor -- node server.js
# In a second terminal, generate load:
autocannon http://localhost:3000 -d 30

# 2. Stop the server (Ctrl+C) — clinic generates HTML report automatically

# 3. Flame graph for CPU hotspots
clinic flame -- node server.js
autocannon http://localhost:3000 -d 30

# 4. Bubbleprof for async bottlenecks
clinic bubbleprof -- node server.js
autocannon http://localhost:3000 -d 30

Common Leak Patterns & Fixes

1. Event Listener Leaks

// BAD: adds listener on every request — never removed
app.get('/stream', (req, res) => {
  emitter.on('data', (chunk) => res.write(chunk));  // LEAK
});

// GOOD: remove listener on request close
app.get('/stream', (req, res) => {
  const handler = (chunk) => res.write(chunk);
  emitter.on('data', handler);
  req.on('close', () => emitter.off('data', handler));  // cleanup
});

// GOOD: use once() for one-time events
emitter.once('connected', () => init());

// Monitor listener counts
emitter.setMaxListeners(20);  // warns when exceeded (default: 10)
console.log(emitter.listenerCount('data'));

2. Unbounded Caches

// BAD: Map grows indefinitely
const cache = new Map();
app.get('/data/:id', (req, res) => {
  const result = expensiveCompute(req.params.id);
  cache.set(req.params.id, result);  // LEAK: never evicted
  res.json(result);
});

// GOOD: LRU cache with max size
import LRU from 'lru-cache';
const cache = new LRU({ max: 1000, ttl: 1000 * 60 * 5 }); // 1000 entries, 5min TTL
app.get('/data/:id', (req, res) => {
  if (cache.has(req.params.id)) return res.json(cache.get(req.params.id));
  const result = expensiveCompute(req.params.id);
  cache.set(req.params.id, result);
  res.json(result);
});

3. Closure Capturing Large Data

// BAD: callback captures entire largeData in closure
async function processReport(id: string) {
  const largeData = await fetchLargeDataset(id);  // 50MB object
  const summary = largeData.items.length;

  // This setTimeout keeps largeData alive for 5 minutes!
  setTimeout(() => {
    console.log(`Report ${id} processed: ${summary} items`);
    // largeData is captured in closure even though unused
  }, 300_000);
}

// GOOD: capture only what you need
async function processReport(id: string) {
  const largeData = await fetchLargeDataset(id);
  const summary = largeData.items.length;
  // largeData is no longer referenced after this line — GC can collect it
  setTimeout(() => console.log(`Report ${id}: ${summary} items`), 300_000);
}

Production Memory Monitoring

// Prometheus metrics for memory (with prom-client)
import client from 'prom-client';

const heapGauge = new client.Gauge({
  name: 'nodejs_heap_used_bytes',
  help: 'Node.js heap used in bytes',
});

setInterval(() => {
  heapGauge.set(process.memoryUsage().heapUsed);
}, 15_000).unref();

// Alert in Grafana when:
// - heapUsed grows > 10MB/hour for 3 hours straight
// - heapUsed > 80% of --max-old-space-size limit

# Start with memory ceiling (crash fast rather than degrade)
node --max-old-space-size=512 server.js  # 512MB limit

Frequently Asked Questions

How do I know if my Node.js app has a memory leak?▼

Signs of a memory leak: heap memory usage grows continuously under steady traffic and never decreases, the process eventually crashes with JavaScript heap out of memory, or response times degrade over time as GC pressure increases. Monitor with process.memoryUsage().heapUsed over time. If it grows by 10MB+ per hour under constant load and doesn't plateau, you have a leak. Tools: Node.js --inspect flag + Chrome DevTools heap snapshots, clinic.js, or process.memoryUsage() logging in production.

What are the most common causes of Node.js memory leaks?▼

The most common causes: (1) Event listeners added but never removed — every addListener without a corresponding removeListener leaks. (2) Unbounded caches — Maps or arrays that grow without eviction (use LRU cache). (3) Closures capturing large objects — inner functions referencing outer scope objects prevent GC. (4) Global variables accumulating data — never append to global arrays/objects in request handlers. (5) setInterval/setTimeout callbacks that reference growing data. (6) Circular references in custom objects (rarely an issue in modern V8, but still possible).

How do I take a heap snapshot in Node.js?▼

Start your app with node --inspect server.js. In Chrome, open chrome://inspect and click your target. Open Memory tab → take a heap snapshot. To find leaks: take snapshot 1, perform the leaking action N times, take snapshot 2. Switch the comparison mode to 'Comparison' and sort by Delta count or retained size. Objects that keep growing indicate the leak source. The Retained Size column shows how much memory would be freed if the object were GC'd.

What is clinic.js and how does it help with Node.js performance?▼

Clinic.js is a suite of diagnostic tools for Node.js: Clinic Doctor detects performance issues (event loop delays, memory leaks, CPU profiling) automatically. Clinic Flame generates an interactive flame graph for CPU profiling. Clinic Bubbleprof visualizes async operations and identifies bottlenecks. Run npx clinic doctor -- node server.js, generate load with a tool like autocannon, then stop and clinic generates an HTML report. It's the fastest way to get a high-level diagnosis without deep manual instrumentation.

How do I detect memory leaks in production Node.js?▼

For production, use process.memoryUsage() to log heap metrics to your monitoring system (Datadog, Prometheus, CloudWatch) every 30-60 seconds. Alert when heapUsed exceeds a threshold or grows by more than X MB/hour. Use --max-old-space-size to set a memory ceiling and expose OOM crashes in metrics. For heap snapshots in production, use the node:v8 module: const v8 = require('v8'); v8.writeHeapSnapshot() creates a snapshot file you can download and analyze offline. Some APM tools (Elastic APM, New Relic) can capture heap snapshots remotely.

What is an event listener leak and how do I prevent it?▼

An event listener leak occurs when addListener is called repeatedly without the corresponding removeListener, causing the callback (and any closure variables it captures) to accumulate in memory. Common patterns: adding event listeners inside request handlers (runs on every request), adding listeners inside component-like functions that get recreated. Fix: use emitter.once() for one-time listeners, always pair addListener with removeListener in cleanup code, use AbortController to remove multiple listeners at once, and monitor with emitter.listenerCount() or set a lower maxListeners limit to trigger warnings early.

Share this tool: X / Twitter LinkedIn
✓ 60,000+ developers use DevPlaybook ★★★★★ 4.8 / 5 on Gumroad ✓ MIT licensed · Instant download ✓ No login required

Related Products

on Gumroad

You Might Also Need

More Free Tools

JavaScript Memory Leak Detector Web Performance Checklist JS Prototype Chain Visualizer AI Agent Memory Planner JavaScript Formatter & Minifier package.json Generator Kubernetes Resource Calculator Lighthouse Score Simulator Web Vitals Budget Planner Core Web Vitals Guide Code Tools GitHub Actions Workflow Generator package.json Validator & Audit Prettier Config Generator Regex Visualizer Regex Tester Pro Regex Tester TypeScript Playground TypeScript to JavaScript Converter Dockerfile Generator Web Vitals Scorer Biome Config Generator Webhook Tester SQL Index Advisor Kubernetes Resource Estimator HAR Diff Analyzer Kubernetes Resource Quota Calculator MySQL Query Explainer PostgreSQL EXPLAIN Analyzer K8s Resource Limits Calculator CI/CD Pipeline Generator TypeScript tsconfig.json Generator Regex Flags Explainer k6 Load Test Script Generator HTTP/2 Server Push Headers Builder NATS Connection String Builder Web Vitals Analyzer Bundle Size Estimator Database Index Analyzer NPM Bundle Size Estimator Jest Testing Framework Guide Core Web Vitals Calculator Database Index Advisor Kubernetes Resource Calculator Bundle Size Impact Calculator HTML5 Canvas API Playground Game Loop Calculator Bezier Curve Editor Particle System Config Generator LLM Benchmark Calculator OpenTelemetry Trace Builder k6 Script Generator API Test Case Generator Core Web Vitals Budget Calculator Bundle Size Budget Calculator Image Optimization Calculator TTFB Analyzer Database Query Cost Analyzer React Component Generator

Related Articles

Enjoying this tool? Get more like it.

Weekly developer tool guides, productivity playbooks & AI tricks. Free. No spam.

DevPlaybook Pro 7 days free

Unlock AI-Powered Dev Tools

  • ⚡ AI Code Review, Doc Generator & SQL Builder
  • ⚡ All premium templates & early access
  • ⚡ Member discounts on Gumroad products
Start Free Trial →

$9/mo after trial · Cancel anytime

Want the full toolkit?

Get DevPlaybook Pro

Every template, guide, boilerplate, and automation script in one bundle. 13 premium products — grab them all at once and save big.

$79
$241 separately Save $162 (67% off)
Get DevPlaybook Pro — $79 →

MIT licensed · Instant download · No subscription

See what's included → Browse all deals →