Skip to content

Zombie MCP server processes consume 50-60% CPU each after session close #311

@camhoccode

Description

@camhoccode

Platform: macOS 14.5 (Darwin 23.5.0), ARM64, Node.js v21.7.3
Claude Code: VSCode extension


Summary

When a Claude Code session closes, context-mode MCP server processes become orphaned and enter an infinite error loop consuming 50-60% CPU each. Multiple sessions accumulate zombie processes, leading to sustained >100% CPU usage.

Steps to Reproduce

  1. Open Claude Code (VSCode extension)
  2. Use context-mode normally (any tool call triggers it)
  3. Close the Claude Code session (close panel, close VSCode, or Ctrl+C)
  4. Open a new Claude Code session
  5. Repeat steps 2-4

Expected Behavior

Old context-mode processes should terminate when the parent Claude Code session dies.

Actual Behavior

Each closed session leaves behind ~6 orphaned processes. The main MCP server worker enters an infinite error loop:

PID 22095: 50.5% CPU, 53MB RSS, running 1h46m (session closed long ago)
PID 24550: 53.1% CPU, 53MB RSS, running 1h31m
PID 38682: 58.6% CPU, 57MB RSS, running 3m (current session — also high CPU)

Total: ~160% CPU for 3 sessions, doing nothing useful.

Root Cause Analysis

1. Missing stdin end handler in StdioServerTransport

The MCP SDK transport (_a class in server.bundle.mjs) only listens for data and error events:

start() {
  this._stdin.on("data", this._ondata);
  this._stdin.on("error", this._onerror);
}

No end or close handler. When parent dies and stdin EOF occurs, the transport doesn't initiate shutdown.

2. Watchdog bypassed by npm wrapper layer

Process tree:

[Claude Code (dead)] → start.mjs → npm exec (PPID=1, orphan but alive) → context-mode server

The watchdog (qC()) checks:

var HC = process.ppid;  // saved at startup
function qC() {
  let t = process.ppid;
  return !(t !== HC || t === 0 || t === 1);
}

But the server's direct parent is npm exec (still alive), not Claude Code. So process.ppid never changes → watchdog never triggers.

3. Infinite error loop (CPU profile evidence)

sample profiling of zombie PID 22095 (1 second, 791 samples):

541/791 (68%) → ReportPendingMessages
  → ErrorStackGetter
    → FormatStackTrace (236 samples)
      → CallSitePrototypeToString
        → SerializeCallSiteInfo
          → ArrayPrototypeJoin

The process is stuck in a tight loop: throw error → format stack trace → catch → retry. V8 heap allocation + GC adds additional CPU overhead.

Suggested Fixes

Fix 1: Add stdin end handler (immediate)

start() {
  this._stdin.on("data", this._ondata);
  this._stdin.on("error", this._onerror);
  this._stdin.on("end", () => this.close());
  this._stdin.on("close", () => this.close());
}

Fix 2: Fix watchdog to check grandparent or use process group

function isParentAlive() {
  // Check if stdin is still readable (most reliable)
  if (process.stdin.destroyed || process.stdin.readableEnded) return false;

  // Also check ppid chain
  const currentPpid = process.ppid;
  if (currentPpid === 0 || currentPpid === 1) return false;
  if (currentPpid !== savedPpid) return false;

  return true;
}

Fix 3: Reduce npm wrapper layer

Instead of npm exec context-mode@latest, start the server directly to eliminate the intermediate process that masks parent death:

// In start.mjs, instead of spawning via npm exec:
import("./server.bundle.mjs");  // Already done when bundle exists

Workaround

Kill orphaned processes manually:

# Kill orphaned context-mode processes (PPID chain leads to init)
ps -eo pid,ppid,command | grep "context-mode" | grep -v grep | while read pid ppid cmd; do
  gppid=$(ps -o ppid= -p $ppid 2>/dev/null | tr -d ' ')
  [ "$gppid" = "1" ] && kill $pid
done

Environment

  • macOS 14.5 (23F79) ARM64
  • Node.js v21.7.3
  • context-mode v1.0.89
  • Claude Code VSCode extension
  • Plugin installed as both local and user scope

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions