Skip to content

bug(v2.4.3): mcp-server orphans accumulate when parent Claude Code exits (154 orphans = 3.5GB RAM on 1 workstation) #57

@pitimon

Description

@pitimon

Summary

In v2.4.3, each Claude Code session spawns a persistent mcp-server.ts bun process for memforge-client. When the parent Claude Code process exits, the MCP child is NOT reaped — it gets adopted by launchd (PPID becomes 1) and keeps running indefinitely, continuing to poll the local claude-mem SQLite DB and retrying the in-memory sync queue.

Over days of normal usage, these orphans accumulate with no self-cleanup, bloating RAM and wasting CPU on stale sync retries that target a sync queue the parent will never drain.

Evidence (single macOS workstation, 2026-04-21)

$ ps -eo pid,ppid,etime,rss,args | awk '$2==1 && /memforge-client\/2\.4\.3\/src\/mcp/' | wc -l
154

$ ps -eo rss,args | awk '/memforge-client\/2\.4\.3\/src\/mcp/ {sum+=$1} END {print sum/1024 " MB"}'
3530 MB       # 154 orphans + 3 living = ~23 MB average each

$ ps -eo pid,ppid,etime,args | awk '$2==1 && /memforge-client\/2\.4\.3\/src\/mcp/ {print}' | sort -k3 -r | head -3
 44463   1   23:06:09   bun /Users/.../memforge-client/2.4.3/src/mcp/mcp-server.ts
 52038   1   23:03:10   bun /Users/.../memforge-client/2.4.3/src/mcp/mcp-server.ts
 53271   1   23:02:59   bun /Users/.../memforge-client/2.4.3/src/mcp/mcp-server.ts
  • 154 orphan MCPs accumulated over ~24 hours
  • 3.5 GB resident set size (~23 MB per process)
  • Oldest orphan: 23 hours old, PPID=1 (launchd)
  • Only 3 MCPs had living parents — matching the 3 active Claude Code sessions

Cleanup restored 1.4 GB free memory (was 214 MB):

$ ps -eo pid,ppid,args | awk '$2==1 && /memforge-client\/2\.4\.3\/src\/mcp/ {print $1}' | xargs kill -TERM
# All 154 responded to SIGTERM within 2 seconds.

Expected behavior

When the parent Claude Code process exits, the MCP server should exit within seconds — either because:

  • Claude Code sends a stdio close/EOF on the MCP pipe and the server shuts down on stdin EOF, or
  • The server polls for parent liveness and exits on orphan detection (process.ppid === 1), or
  • A SIGHUP on parent exit triggers graceful shutdown.

Actual behavior

  • stdin EOF does not trigger exit — the bun process stays alive
  • No parent-liveness watchdog
  • The in-process sync poller keeps running with adaptive polling (per comments in src/sync/sync-poller.ts), retrying items from ~/.memforge/.sync-queue.json with ever-growing retryCount (one item reached retryCount: 17,840 in our env)
  • Parent exit leaves the child as a launchd-adopted orphan

Suggested fixes (pick one or combine)

A — stdin EOF shutdown (minimal, standard for MCP stdio servers):

process.stdin.on('end', () => {
  logger.info('stdin EOF — parent gone, shutting down');
  // flush pending sync, then process.exit(0)
});
process.stdin.on('close', () => process.exit(0));

B — parent-liveness watchdog (defensive, catches edge cases where stdin stays open):

const PARENT_PID = process.ppid;
setInterval(() => {
  if (process.ppid !== PARENT_PID && process.ppid === 1) {
    logger.info('Adopted by launchd — parent died, exiting');
    // flush pending sync, then process.exit(0)
  }
}, 30_000);

C — SIGHUP handler:

process.on('SIGHUP', () => {
  // flush + exit(0)
});

Most robust = A + B combined. Claude Code's MCP protocol closes stdio on session end, so A handles the normal case. B catches the pathological case where stdio stays open (e.g. kernel bug, zombie pipe).

Environment

  • OS: macOS (Darwin, Apple Silicon)
  • Shell: zsh
  • Bun: 1.3.5
  • memforge-client: 2.4.3
  • claude-mem: 12.3.7 (unrelated — claude-mem's own mcp-server.cjs does exit cleanly)
  • Parent Claude Code: various versions over 24h, sessions opened/closed normally

Related

Why this matters

On machines with many Claude Code sessions over time:

  • 3.5 GB wasted per ~24h of normal usage (scales linearly)
  • Stale sync-poller retries hit the memforge server with retryCount into the tens of thousands
  • Makes claude-mem.db file locking noisier (N+1 readers instead of 2-3)

Happy to test any patch against our workstation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions