Detect and Fix Memory Leaks with Claude Code

Written by Michael Lip · Solo founder of Zovo · $400K+ on Upwork · 100% JSS Join 50+ builders · More at zovo.one

The Problem

Your application’s memory usage grows over time. In Node.js, the process eventually crashes with:

FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory

In the browser, tabs become sluggish and eventually unresponsive. You know there is a memory leak but finding it manually requires deep knowledge of heap profiling tools.

Quick Fix

Ask Claude Code to scan for the most common memory leak patterns:

Scan my codebase for common memory leak patterns:
- Event listeners that are added but never removed
- setInterval/setTimeout without cleanup
- Growing arrays or maps that are never cleared
- Closures capturing large objects
- Missing cleanup in React useEffect hooks

Claude Code will search your code and flag specific instances with file paths and line numbers.

What’s Happening

A memory leak occurs when your application allocates memory that is never released back to the garbage collector. In JavaScript, this typically happens when objects remain referenced even after they are no longer needed.

The garbage collector frees memory for objects that have no references pointing to them. A leak means something is holding a reference that prevents collection. Over time, these unreachable-but-referenced objects accumulate and consume all available memory.

Step-by-Step Detection and Fix

Step 1: Identify symptoms

Ask Claude Code to add memory monitoring to your application:

// For Node.js - add to your server startup
setInterval(() => {
  const usage = process.memoryUsage();
  console.log({
    rss: `${Math.round(usage.rss / 1024 / 1024)} MB`,
    heapUsed: `${Math.round(usage.heapUsed / 1024 / 1024)} MB`,
    heapTotal: `${Math.round(usage.heapTotal / 1024 / 1024)} MB`,
    external: `${Math.round(usage.external / 1024 / 1024)} MB`,
  });
}, 10000);

If heapUsed consistently grows over time (not just during request spikes), you have a memory leak.

Step 2: Take heap snapshots

Ask Claude Code to set up heap snapshot endpoints:

import v8 from 'v8';
import fs from 'fs';

// Add this endpoint to your Express/Fastify server
app.get('/debug/heap-snapshot', (req, res) => {
  const snapshotPath = `/tmp/heap-${Date.now()}.heapsnapshot`;
  const snapshotStream = v8.writeHeapSnapshot(snapshotPath);
  res.json({ path: snapshotStream });
});

Take snapshots at intervals and compare them:

# Take snapshot after startup
curl http://localhost:3000/debug/heap-snapshot

# Wait, generate some load, then take another
curl http://localhost:3000/debug/heap-snapshot

Step 3: Analyze common leak patterns

Ask Claude Code to check for these specific patterns:

Event listener leaks:

// LEAK: Listener added on every request, never removed
app.get('/stream', (req, res) => {
  const handler = (data) => res.write(data);
  eventEmitter.on('data', handler);
  // Missing: req.on('close', () => eventEmitter.off('data', handler));
});

// FIXED:
app.get('/stream', (req, res) => {
  const handler = (data: Buffer) => res.write(data);
  eventEmitter.on('data', handler);
  req.on('close', () => {
    eventEmitter.off('data', handler);
  });
});

Cache without bounds:

// LEAK: Cache grows forever
const cache = new Map<string, object>();

function getData(key: string): object {
  if (!cache.has(key)) {
    cache.set(key, expensiveComputation(key));
  }
  return cache.get(key)!;
}

// FIXED: Use LRU cache with max size
import { LRUCache } from 'lru-cache';

const cache = new LRUCache<string, object>({
  max: 500,
  ttl: 1000 * 60 * 5, // 5 minutes
});

Closure capturing large scope:

// LEAK: Closure keeps entire 'largeData' alive
function processData() {
  const largeData = loadGigabyteFile();
  const summary = computeSummary(largeData);

  return function getSummary() {
    // Only uses 'summary' but 'largeData' stays in memory
    return summary;
  };
}

// FIXED: Null out the reference
function processData() {
  let largeData: Buffer | null = loadGigabyteFile();
  const summary = computeSummary(largeData);
  largeData = null; // Allow GC to collect

  return function getSummary() {
    return summary;
  };
}

React useEffect missing cleanup:

// LEAK: Interval never cleared
useEffect(() => {
  const interval = setInterval(() => {
    fetchUpdates();
  }, 5000);
  // Missing return cleanup
}, []);

// FIXED:
useEffect(() => {
  const interval = setInterval(() => {
    fetchUpdates();
  }, 5000);
  return () => clearInterval(interval);
}, []);

Step 4: Automated leak detection

Ask Claude Code to add leak detection to your test suite:

// jest.setup.ts
import { memoryUsage } from 'process';

let initialMemory: number;

beforeAll(() => {
  global.gc?.(); // Run with --expose-gc
  initialMemory = memoryUsage().heapUsed;
});

afterAll(() => {
  global.gc?.();
  const finalMemory = memoryUsage().heapUsed;
  const growth = finalMemory - initialMemory;
  const growthMB = growth / 1024 / 1024;

  if (growthMB > 50) {
    console.warn(`Possible memory leak: heap grew by ${growthMB.toFixed(1)} MB during tests`);
  }
});

Run tests with garbage collection exposed:

node --expose-gc node_modules/.bin/jest --runInBand

Step 5: Fix and verify

After Claude Code identifies and fixes leaks, verify with a load test:

# Install autocannon for load testing
npm install -g autocannon

# Baseline memory measurement
curl http://localhost:3000/debug/heap-snapshot

# Generate sustained load
autocannon -d 60 -c 50 http://localhost:3000/api/endpoint

# Post-load measurement
curl http://localhost:3000/debug/heap-snapshot

Compare the before and after heap sizes. A healthy application’s memory should plateau, not continuously grow.

Prevention

Add these rules to your CLAUDE.md to prevent future leaks:

## Memory Safety Rules
- Every addEventListener must have a corresponding removeEventListener
- Every setInterval must have a corresponding clearInterval in cleanup
- Every useEffect that creates subscriptions must return a cleanup function
- Use bounded data structures (LRU cache) instead of unbounded Maps/arrays
- Never store request/response objects in module-level variables


Claude Code is expensive because it's reading your entire codebase every time. A CLAUDE.md tells it what matters upfront — architecture, conventions, boundaries. Less scanning. Fewer wrong turns. Lower bills. I spend $200+/month on Claude subs. These configs are how I keep the output worth the cost. **[Get the configs →](https://zovo.one/lifetime?utm_source=ccg&utm_medium=cta-perf&utm_campaign=claude-code-memory-leak-detection-guide)** $99 once. Pays for itself in saved tokens within a week.