Managing Memory Leaks When Running Multiple Claude Code Agents in Parallel
Managing Memory Leaks When Running Multiple Claude Code Agents in Parallel
Running one Claude Code agent is fine. Running five in parallel on the same machine turns memory leaks from a minor annoyance into a machine-killing problem. Orphaned node processes pile up, and within a few hours your Mac is swapping to disk.
Why This Happens
Each Claude Code session spawns multiple node processes - MCP servers, language servers, file watchers, and the Claude Code process itself. When a session ends or crashes, some of these processes do not clean up after themselves. Their parent process exits but they stay alive, adopted by PID 1 (launchd on macOS).
A Node.js memory leak is technically an orphan block of memory on the heap that is no longer used by the application because it has not been released by the garbage collector. In the context of Claude Code parallel sessions, the leak is more literal: entire processes that should have exited but did not. The garbage collector cannot help with a process that refuses to exit.
With one agent, you might lose a few hundred megabytes from a stale MCP server. With five agents running continuously for hours, you can accumulate gigabytes of orphaned processes. The symptoms are predictable: fans spin up, builds get slower, and eventually something gets OOM-killed.
Identifying Orphaned Processes
An orphaned process has one of two characteristics:
- Its parent PID is 1 (adopted by launchd after its real parent exited)
- Its parent is a process ID that no longer exists
You can check this with a simple bash pattern:
#!/bin/bash
# Find orphaned node processes from Claude Code sessions
echo "=== Orphaned node processes (PPID=1) ==="
ps aux | awk '$3 == "1" && /node/ {print $0}'
echo ""
echo "=== Node processes with dead parents ==="
for pid in $(pgrep -f "node.*mcp\|node.*claude"); do
ppid=$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d ' ')
if [ -n "$ppid" ] && [ "$ppid" != "1" ]; then
# Check if parent still exists
if ! kill -0 "$ppid" 2>/dev/null; then
echo "Orphan: PID $pid (parent $ppid is dead)"
ps -p "$pid" -o pid,ppid,rss,command 2>/dev/null
fi
fi
done
The rss column (Resident Set Size) tells you how much physical memory each process is using. A stale MCP server consuming 200MB that has been running for six hours with no parent process is a clear target.
The Cleanup Script
This script runs as a cron job or launchd timer and kills orphaned Claude Code-related processes:
#!/bin/bash
# claude-cleanup.sh - Kill orphaned node processes from Claude Code sessions
# Run via: crontab -e -> */90 * * * * /usr/local/bin/claude-cleanup.sh
LOG_FILE="$HOME/.claude/cleanup.log"
KILLED_COUNT=0
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
}
log "Starting cleanup run"
# Pattern: MCP servers spawned by Claude Code
PATTERNS=(
"node.*@modelcontextprotocol"
"node.*mcp-server"
"node.*claude.*server"
)
for pattern in "${PATTERNS[@]}"; do
while IFS= read -r line; do
pid=$(echo "$line" | awk '{print $2}')
ppid=$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d ' ')
# Skip if we couldn't get the PID or it's already gone
[ -z "$pid" ] || [ -z "$ppid" ] && continue
# Skip PID 1 children if they were recently created (< 10 min old)
etime=$(ps -o etime= -p "$pid" 2>/dev/null | tr -d ' ')
if [ "$ppid" = "1" ]; then
# Only kill processes running > 10 minutes with no parent
# etime format: [[DD-]HH:]MM:SS
minutes=$(echo "$etime" | awk -F: '{print $(NF-1)}')
[ "${minutes:-0}" -lt 10 ] && continue
log "Killing orphan PID $pid (pattern: $pattern, etime: $etime, mem: $(ps -o rss= -p $pid 2>/dev/null)k)"
kill -TERM "$pid" 2>/dev/null
sleep 1
kill -KILL "$pid" 2>/dev/null
KILLED_COUNT=$((KILLED_COUNT + 1))
fi
done < <(ps aux | grep -E "$pattern" | grep -v grep)
done
log "Cleanup complete. Killed $KILLED_COUNT processes."
# Also log current node process count for trend tracking
NODE_COUNT=$(pgrep -c -f "node" 2>/dev/null || echo 0)
log "Current node process count: $NODE_COUNT"
Run this every 90 minutes via cron:
# Add to crontab: crontab -e
*/90 * * * * /usr/local/bin/claude-cleanup.sh
Monitoring Memory Trends
The cleanup script alone is reactive. For proactive monitoring, track the node process count over time. If it consistently grows between cleanup runs, you have a leak rate that exceeds the cleanup interval.
# Watch node process memory in real-time
watch -n 5 'ps aux | grep node | grep -v grep | awk "{sum += \$6} END {print \"Total RSS: \" sum/1024 \" MB, Count: \" NR}"'
Node's built-in process.memoryUsage() reports heap statistics. For MCP servers you control, adding periodic memory logging helps identify which servers are growing:
// Add to MCP server initialization
setInterval(() => {
const mem = process.memoryUsage();
if (mem.heapUsed > 200 * 1024 * 1024) { // 200MB threshold
console.error(`[MEMORY WARNING] heapUsed: ${Math.round(mem.heapUsed / 1024 / 1024)}MB`);
}
}, 60000); // Check every minute
Prevention Habits
Beyond cleanup, a few practices reduce the accumulation rate:
Close sessions through the Claude Code UI rather than closing the terminal. When you kill a terminal window, the shell exits but child processes may not receive the signal cleanly. Using the quit command inside Claude Code gives the session a chance to clean up its subprocess tree.
Use separate terminal tabs rather than background processes. Background processes with & are harder to track and more likely to become orphaned. Visible processes in tabs are easier to notice and kill deliberately.
Monitor the process tree before starting new sessions. Running pgrep -c -f "node" before launching another agent session tells you the baseline. If the count is already 40+ processes, the previous session may not have cleaned up fully.
Set reasonable MCP server count. Each Claude Code session can connect to many MCP servers. Start with the minimum set you need rather than enabling everything. Each server is another process that can become orphaned.
The Underlying Issue
Long-running agent sessions were not the primary design target when MCP server process management was built. The expectation was shorter, more bounded interactions rather than continuous multi-session operation over many hours.
Until the tooling improves, a cleanup script is essential infrastructure for anyone running parallel agents continuously. It is not glamorous, but a Mac that stays responsive is worth the 30 minutes to set it up.
- Claude Code's Real Advantage Is the Harness
- Managing Parallel Claude Agents
- LLM Costs and Monthly Breakdown for Agents
Fazm is an open source macOS AI agent. Open source on GitHub.