Memory Leak Hunter

v1.0.0

Detect and diagnose memory leaks in applications. Analyze heap snapshots, track memory growth over time, identify leaked objects (event listeners, closures,...

0· 19· 1 versions· 0 current· 0 all-time· Updated 5h ago· MIT-0

Install

openclaw skills install memory-leak-hunter

Memory Leak Hunter

Find what's eating your application's memory. Analyze heap growth patterns, compare snapshots, identify retained objects (event listeners, closures, timers, detached DOM nodes, unclosed connections), and generate specific fixes — not just "you have a leak" but exactly where and why.

Use when: "memory leak", "memory keeps growing", "OOM killed", "heap dump analysis", "why is memory usage so high", "application memory keeps increasing", or when memory usage doesn't stabilize under steady traffic.

Commands

1. detect — Find Memory Leaks

Step 1: Measure Memory Growth

# Node.js — track heap over time
node -e "
const used = () => process.memoryUsage();
setInterval(() => {
  const m = used();
  console.log(JSON.stringify({
    rss: (m.rss / 1048576).toFixed(1) + ' MB',
    heap: (m.heapUsed / 1048576).toFixed(1) + ' MB',
    external: (m.external / 1048576).toFixed(1) + ' MB'
  }));
}, 5000);
" &
# Send load for 2 minutes, then observe if memory returns to baseline
# Python — track memory
python3 -c "
import tracemalloc, time
tracemalloc.start()
# ... run your code ...
snapshot = tracemalloc.take_snapshot()
for stat in snapshot.statistics('lineno')[:10]:
    print(stat)
"
# Any process — watch RSS over time
watch -n 5 "ps -o pid,rss,vsz,comm -p \$(pgrep -f 'your-app')"
# Container memory
kubectl top pods --sort-by=memory 2>/dev/null
docker stats --no-stream 2>/dev/null

Step 2: Heap Snapshot Analysis (Node.js)

# Take heap snapshots at intervals
# Method 1: via inspector protocol
node --inspect=9229 app.js &
# Send signal to take snapshot
kill -USR2 $(pgrep -f app.js)

# Method 2: programmatic
node -e "
const v8 = require('v8');
const fs = require('fs');
const snap1 = v8.writeHeapSnapshot();
console.log('Snapshot 1:', snap1);
// ... do work ...
const snap2 = v8.writeHeapSnapshot();
console.log('Snapshot 2:', snap2);
"

Compare two snapshots — objects that grew between them are leak candidates:

  • Sort by retained size delta
  • Focus on objects with increasing count but no corresponding decrease
  • Look for unexpected string retention (often log buffers or error messages)

Step 3: Common Leak Patterns

Event listeners not removed:

rg "addEventListener|\.on\(|\.once\(" --type ts --type js -g '!node_modules' 2>/dev/null | \
  while read line; do
    file=$(echo "$line" | cut -d: -f1)
    # Check if there's a corresponding removeEventListener
    rg "removeEventListener|\.off\(|\.removeListener\(" "$file" 2>/dev/null | wc -l
  done

Closures retaining scope:

# Find closures in loops or repeated callbacks that capture large objects
rg -U "setInterval|setTimeout|\.on\(.*=>\s*\{" --type ts --type js -g '!node_modules' 2>/dev/null | head -20

Unclosed resources:

# Find open without corresponding close
rg "\.open\(|createConnection|createPool|createClient" --type ts --type js --type py -g '!node_modules' 2>/dev/null
# Check for .close() or .end() or .destroy() in same files

Detached DOM nodes (browsers):

# Find DOM manipulation without cleanup
rg "createElement|appendChild|insertBefore" --type ts --type js -g '!node_modules' 2>/dev/null
# Check for corresponding removeChild or cleanup

Growing collections:

# Find arrays/maps/sets that only push/add, never clear/delete
rg "\.push\(|\.set\(|\.add\(" --type ts --type js -g '!node_modules' -g '!*.test.*' 2>/dev/null | head -20

Step 4: Generate Report

# Memory Leak Analysis

## Evidence
- RSS grew from 120 MB to 850 MB over 6 hours under steady load
- Heap used grew from 80 MB to 620 MB
- No corresponding increase in active connections or requests
- Memory does NOT decrease after traffic stops → confirmed leak

## Leaked Objects (from heap diff)
1. **String** — +45,000 instances, +120 MB retained
   Source: `logger.ts:23` — error messages appended to in-memory buffer without rotation
   Fix: Add max buffer size or use streaming logger

2. **EventEmitter listeners** — +2,300 instances
   Source: `websocket.ts:67` — `socket.on('message')` handlers added on reconnect, never removed
   Fix: Call `socket.removeAllListeners('message')` before re-adding

3. **Promise** — +8,000 unresolved
   Source: `api-client.ts:45` — timeout promises created but never rejected on disconnect
   Fix: Add AbortController with timeout cleanup

## Recommendations
1. Add `--max-old-space-size=512` to Node.js flags as safety limit
2. Implement log rotation (max 1000 entries in memory buffer)
3. Add connection cleanup in WebSocket reconnection handler
4. Monitor `process.memoryUsage()` and alert at 70% of limit

2. profile — Continuous Memory Profiling

Set up lightweight continuous memory monitoring:

  • Periodic heap stats logging (every 30s, low overhead)
  • Alert thresholds (absolute MB, growth rate MB/min)
  • Automatic heap snapshot on approaching OOM
  • Integration with APM tools (Datadog, New Relic, Prometheus)

3. fix — Generate Fix for Common Patterns

For each identified leak pattern, provide the exact code fix:

  • Event listener cleanup in component lifecycle
  • WeakMap/WeakRef for caches that should allow GC
  • AbortController for cancellable operations
  • Connection pool with max size and idle timeout
  • Buffer/collection with bounded size and LRU eviction

Version tags

latestvk97dwdpabjm82gkbw7f487abr185xm9k