0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude CodeがRAMを食い尽くす(カーネルパニックする)のは、トークンの肥大でなくMCPプロセスの漏れ——見つけ方・安全な回収・上限の付け方

0
Posted at

Claude Codeを使っていると、メモリがじわじわ食われて、しまいにはマシンがスワップで這うように遅くなる。ひどいと固まる、カーネルパニックする、再起動を強いられる。こういう時、よく見る「Claude CodeがRAMを食う」系の対処(トークンやログの掃除)では直りません。あれはコンテキストの大きさの話で、ここで起きているのは別の事故です。

正体は、セッションが終わったのに生き残ったMCPサーバやサブエージェントの子プロセスが溜まって、RAMとswapを食い潰すことです。

これは実在の、繰り返し報告されている事故です。2026年5月から6月の起票だけでも、#64366(11個のMCPサーバが約300プロセスに増え、swap 24GB、4日で4回のカーネルパニック)、#68647(並列のサブエージェントが子プロセスを漏らし128GBを枯渇させmacOSのwatchdogがパニック)、#66020#68933#61748(150以上のプロセス・31GB)と、10件を超えます。同じ形が Copilot CLI や Codex でも報告されていて、これは一つの道具のバグでなく、エージェントの時代の構造的な事故です。

なぜ起きるのか

Claude Code(やサブエージェント)がMCPサーバを起動すると、そのサーバは子プロセスになります。セッションやサブエージェントが終わった時、本来はそのサーバも片付けられるはずです。ところが、片付けられずに残ることがある。残ったサーバはPID 1に再ペアレントされて、メモリを抱えたまま動き続けます。何日かセッションを回すうちに、死んだセッションのMCPサーバが何十個もメモリを取り合い、最後はOOM killer(かパニック)で終わります。

手順1 漏れているか見る(読むだけ・安全)

肝心なのは、「PID 1に再ペアレントされている」かつ「MCPサーバらしい見た目」の両方を満たすプロセスです。再ペアレントだけでは何の意味もありません。あなたの正当なデーモンもPID 1の子だからです。だから、PID 1のプロセスを見境なく殺してはいけません。

ps -eo pid,ppid,etimes,rss,args | awk '$2==1 && /@modelcontextprotocol|mcp-server-|[/ ]mcp[ _-]/ {printf "%s  age=%ss  %sMB  %s\n",$1,$3,int($4/1024),$0}'

一行が一つの漏れたサーバで、PID・動いている秒数・実メモリ・コマンドが出ます。これが何個も出るなら、それがあなたのRAMです。

手順2 安全に回収する

次のスクリプトは、漏れを一覧にし、MCPの署名に一致するものだけを対象にします。既定はdry-run(何を消すか見せるだけで、何も消しません)。--killを付けた時だけ回収します。

#!/bin/bash
# mcp-orphan-reaper.sh — PID 1 に再ペアレントされた漏れMCPサーバを一覧(既定)/回収(--kill)
# MCP署名に一致しないプロセスには一切手を出さない
set -u
KILL=0; MIN_AGE=300
while [ $# -gt 0 ]; do case "$1" in
  --kill) KILL=1 ;; --min-age) shift; MIN_AGE="${1:-300}" ;; esac; shift; done
SIG='@modelcontextprotocol|mcp-server-|modelcontextprotocol|[/ ]mcp([ _-]|$)|uvx[^|]*mcp|npx[^|]*mcp'
mapfile -t ROWS < <(ps -eo pid=,ppid=,etimes=,rss=,args= \
  | awk -v sig="$SIG" -v m="$MIN_AGE" '$2==1 && $3>=m {
      a=""; for(i=5;i<=NF;i++) a=a (i>5?" ":"") $i; if(a~sig) print $1"\t"$3"\t"$4"\t"a }')
[ "${#ROWS[@]}" -eq 0 ] && { echo "漏れたMCPサーバは見つかりません(PID 1 + MCP署名, ${MIN_AGE}秒以上)。"; exit 0; }
tot=0; echo "漏れたMCP/エージェントのサーバ:"; printf '  %-8s %-7s %-7s %s\n' PID 'AGE' 'MB' CMD
for r in "${ROWS[@]}"; do IFS=$'\t' read -r p a rss c <<<"$r"; tot=$((tot+rss))
  printf '  %-8s %-7s %-7s %s\n' "$p" "$a" "$((rss/1024))" "$(echo "$c"|cut -c1-72)"; done
echo "  --- ${#ROWS[@]} 個, 約 $((tot/1024)) MB 回収可。"
if [ "$KILL" -eq 1 ]; then for r in "${ROWS[@]}"; do kill -TERM "${r%%$'\t'*}" 2>/dev/null \
  && echo "killed ${r%%$'\t'*}"; done
else echo "dry-run。確認したら --kill で回収、または kill <PID> で個別に。"; fi

私はこれをWSL2の実機で確かめました。偽のMCP署名つきの孤児を検出し、MCP署名でない正当なPID 1のデーモンには手を出さず、きれいに回収します。安全のすべては署名の一致で、MCPサーバらしく見えないプロセスには絶対に信号を送りません。

手順3 そもそも上限を付けて、二度とパニックさせない

回収は後始末です。本当の対策は天井を付けることです。MCPを多く使う重いセッションを、メモリの上限の下で起動しておけば、暴走した漏れがあっても上限に当たって死ぬだけで、マシン全体を道連れにしません。

# systemd のホスト: 8GBでセッションのcgroupを殺す(スワップ死でなく)
systemd-run --scope -p MemoryMax=8G claude

# どのシェルでも: 仮想メモリの上限(KB)をセッションと子に課す
( ulimit -v 8000000; claude )

天井は、実際の作業量より上で、総RAMより十分下に取ります。これで漏れは、再起動でなく、ただの煩わしさで済みます。

次のセッションの前に気づく

一番安いのは、次のセッションの始まりに、もう漏れが溜まっているかを警告してもらうことです。これをする(警告だけで何も殺さない・署名で安全に絞る)SessionStartのhook mcp-orphan-leak-warner.sh を、無料の安全装置の集まり cc-safe-setup に入れました。上のスクリプトは導入なしでそのまま使えます。

「Claude CodeがRAMを食う」と思ったら、まずpsで、PID 1のMCPサーバが溜まっていないかを見る。トークンの掃除の前に、そこを見てください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?