2026-06-27 の Claude Code アップデートに、地味ですが見逃せない一文がありました。ストリーミングと長時間セッション中の CPU・メモリ使用量が下がった、という改善です。新機能の派手さはありませんが、無人で何時間も回し続ける運用をしている身からすると、この種の底上げが一番ありがたく感じます。
私自身、複数サイトの記事をヘッドレスの Claude Code で自動投稿していて、一番怖いのは「途中で重くなって、後半のジョブだけ取りこぼす」事故です。クラッシュなら気づけますが、徐々に遅くなって最後の数本が間に合わなかった、という静かな失敗は気づきにくい。今回の省メモリ化はその不安を一段やわらげてくれますが、アプリ側の更新だけに頼って観測をやめてしまうと、また同じ落とし穴に戻ります。
やることは三層です。Claude Code の常駐メモリを数十行で定点観測し、重くなり始めを検知し、長セッションをセグメントに割って頭打ちにする。個人開発で無人運用を続けてきた経験から、実際に効いた手順だけを順に並べていきます。
長時間の無人セッションで、何が「重さ」になるのか
まず切り分けたいのは、ここで扱う「メモリ」が二種類あることです。ひとつは Claude が抱える文脈(コンテキストウィンドウ)で、これはトークン課金やレイテンシに効きます。もうひとつはローカルで動く Claude Code プロセスそのものの常駐メモリ(RSS: resident set size)で、これは OS から見たプロセスの重さです。今回の 6/27 の改善が下げたのは主に後者です。文脈の整理術は別の話なので、ここでは一貫して RSS の話に絞ります。
無人運用で RSS が問題になるのは、だいたい次のような形でした。
一本のセッションで何十回もツール呼び出しを重ねると、ストリーミングのバッファや中間状態が積み上がり、RSS がじわじわ伸びる
VM やコンテナのメモリ上限が低いと、伸びた RSS が上限に当たり、OS の OOM キラーにプロセスごと落とされる
落ちないまでも、スワップが始まると全体が遅くなり、後半のジョブが時間内に終わらない
クラッシュは派手なので気づけます。やっかいなのは三番目で、エラーを出さずに「ただ遅くなる」。だからこそ、まずは観測から始めます。
6/27 のアップデートが下げたもの、下げないもの
公開された改善は「ストリーミングと長時間セッション中の CPU・メモリ使用量の削減」です。つまり、同じ作業をしても以前より RSS の立ち上がりが緩やかになる、という底上げです。これは素直にありがたい。
ただし、アプリ側が下げてくれるのはあくまで基礎代謝の部分です。あなたのジョブが一本のセッションに何百回ぶんの作業を詰め込んでいれば、緩やかになった傾きでも、十分な時間をかければ上限には届きます。アップデートは到達を遅らせますが、無限に回せる保証ではありません。観測とセグメント分割は、アプリの改善と矛盾せず重ねて効きます。
考え方としては、アプリの省メモリ化が「坂を緩くする」もので、この記事の運用が「坂の途中で平らに戻す踊り場を作る」もの、と捉えると整理しやすいです。
メモリ使用量を観測する最小の仕掛け
最初の一歩は、特別な監視基盤を入れることではありません。ps で RSS を定期的に書き出すだけで、驚くほど多くのことが分かります。次のスクリプトは、対象プロセスとその子プロセスの RSS 合計を 15 秒ごとに CSV へ追記します。
#!/usr/bin/env bash
# rss-sample.sh — 指定 PID とその子孫の RSS 合計を CSV に追記する
# 使い方: ./rss-sample.sh <root_pid> <out.csv> [interval_sec]
set -euo pipefail
ROOT_PID = " $1 "
OUT = " $2 "
INTERVAL = " ${3 :- 15} "
# 子孫 PID を再帰的に集める
collect_descendants () {
local pid = " $1 "
echo " $pid "
for child in $( pgrep -P " $pid " 2> /dev/null || true ); do
collect_descendants " $child "
done
}
# ヘッダ(初回のみ)
[ -s " $OUT " ] || echo "ts_epoch,iso,proc_count,rss_kb" >> " $OUT "
while kill -0 " $ROOT_PID " 2> /dev/null ; do
pids = "$( collect_descendants " $ROOT_PID " | sort -u )"
count = 0
total = 0
for p in $pids; do
# ps の rss は KB 単位。存在しない PID は無視
kb = "$( ps -o rss= -p " $p " 2> /dev/null | tr -d ' ' || true )"
if [ -n "${ kb :- }" ]; then
total = $(( total + kb ))
count = $(( count + 1 ))
fi
done
printf '%s,%s,%s,%s\n' "$( date +%s)" "$( date -u +%FT%TZ)" " $count " " $total " >> " $OUT "
sleep " $INTERVAL "
done
ヘッドレス実行のラッパーから、Claude Code を起動した直後にこのサンプラーをバックグラウンドで回します。
# run-with-sampling.sh の抜粋
claude -p " $PROMPT " --output-format stream-json > run.log 2>&1 &
CLAUDE_PID = $!
./rss-sample.sh " $CLAUDE_PID " "metrics/rss-$( date +%Y%m%d-%H%M%S).csv" 15 &
SAMPLER_PID = $!
wait " $CLAUDE_PID "
kill " $SAMPLER_PID " 2> /dev/null || true
これだけで、一本のセッションの RSS の時系列が手に入ります。ピーク値、伸びの傾き、平らに戻るかどうか。最初の数回はグラフ化して眺めるだけで十分です。私の環境では、軽い記事生成ジョブ一本で RSS のピークがおおよそ 320〜420 MB の範囲に収まり、何本も連続で同一セッションに積むと、これがゆっくり右肩上がりになっていく様子が見えました。数値そのものより、「平らに戻るか、戻らないか」を見るのが肝心です。
しきい値で気づく — ローリング基準値のウォッチドッグ
固定のしきい値(例えば「600 MB を超えたら警告」)は、環境差で簡単に誤検知します。マシンを変えれば基準が変わるからです。そこで、直近の観測値からローリングで基準を作り、そこからの逸脱で判断します。中央値と中央絶対偏差(MAD)を使うと、一時的なスパイクに引っ張られにくく、安定して「重くなり始め」を捉えられます。
#!/usr/bin/env python3
# rss-watchdog.py — CSV を読み、ローリング中央値+MAD で逸脱を検知する
import csv, sys, statistics
WINDOW = 20 # 直近サンプル数
K = 6.0 # しきい値の係数(MAD の何倍まで許すか)
MIN_SAMPLES = 8 # 基準を作るのに最低限必要なサンプル数
def mad (xs, med):
return statistics.median([ abs (x - med) for x in xs]) or 1.0
def main (path):
rss = []
with open (path) as f:
for row in csv.DictReader(f):
rss.append( int (row[ "rss_kb" ]))
if len (rss) < MIN_SAMPLES :
print ( "not enough samples" ); return 0
# 立ち上がり直後を除いた安定区間を基準にする
baseline = rss[ 2 : 2 + WINDOW ] if len (rss) >= 2 + WINDOW else rss[ 2 :]
med = statistics.median(baseline)
spread = mad(baseline, med)
latest = rss[ - 1 ]
threshold = med + K * spread
drift = (latest - med) / med * 100
print ( f "baseline_median= { med / 1024 :.0f } MB latest= { latest / 1024 :.0f } MB "
f "threshold= { threshold / 1024 :.0f } MB drift= { drift :+.1f } %" )
if latest > threshold:
print ( "ALERT: RSS drifted above rolling baseline" )
return 1
return 0
if __name__ == "__main__" :
sys.exit(main(sys.argv[ 1 ]))
ウォッチドッグの戻り値を運用ループで拾えば、「重くなり始めたら、いまのジョブを安全な切れ目で締めて、次のセグメントへ移る」という判断につなげられます。ここで大事なのは、検知したら即座にプロセスを殺すのではなく、安全な境界まで現在の作業を進めてから区切ることです。途中で殺すと部分成果を失います。
固定しきい値とローリング基準値の違いを整理すると、次のようになります。
観点 固定しきい値 ローリング基準値+MAD
環境差への強さ 弱い (マシンごとに調整が要る)強い (その実行の安定区間を基準にする)
一時スパイク耐性 低い(瞬間値で誤発火) 高い(中央値ベースで吸収)
検知したいもの 絶対量の超過 右肩上がりの傾向(ドリフト)
初期設定コスト 毎環境で要調整 係数 K の調整だけで流用可
長セッションを境界付きセグメントに割る
観測とウォッチドッグは「気づく」ための層です。根本対策は、そもそも一本のセッションに無限に積まないことです。長い無人ジョブを、いくつかの境界付きセグメントに割り、セグメントの切れ目でプロセスを終了させると、RSS はそこで一度リセットされ、頭打ちになります。
ここで効くのが、文脈を保ったままセッションを再開できる仕組みです。セグメントをまたいで作業の連続性が必要なら --resume や --continue で前のセッションを引き継げますし、完全に独立したジョブなら毎回まっさらに起動すればよい。私のサイト自動投稿では「1セグメント = 記事1〜2本ぶん」を目安に切っています。
#!/usr/bin/env bash
# segmented-run.sh — 長いジョブを境界付きセグメントに割って回す
set -euo pipefail
JOBS = ( "記事A生成" "記事B生成" "記事C生成" "記事D生成" )
SEGMENT_SIZE = 2 # 1セグメントあたりのジョブ数
i = 0
session_id = ""
while [ " $i " -lt "${ # JOBS [ @ ]}" ]; do
batch = ( "${ JOBS [ @ ] : i : SEGMENT_SIZE }" )
prompt = "$( printf '%s\n' "${ batch [ @ ]}")"
# 文脈の連続が必要なら --resume、独立なら省略
if [ -n " $session_id " ]; then
claude -p " $prompt " --resume " $session_id " --output-format stream-json > "seg- $i .log" 2>&1
else
claude -p " $prompt " --output-format stream-json > "seg- $i .log" 2>&1
fi
# 次セグメントへ。プロセスは毎回終了するので RSS はここでリセットされる
session_id = "$( grep -o '"session_id":"[^"]*"' "seg- $i .log" | head -1 | cut -d '"' -f4 || true )"
i = $(( i + SEGMENT_SIZE ))
done
ポイントは、セグメントの切れ目で claude プロセスが必ず終了することです。次のセグメントは新しいプロセスで立ち上がるので、前のセグメントが抱えていた常駐メモリは OS に返ります。坂を緩くするのがアプリの省メモリ化なら、この踊り場は運用側で作る平らな区間です。
Before / After — 一本の長セッション vs セグメント分割
実際に同じ4本ぶんの作業を、一本の長セッションで通した場合と、2本ずつのセグメントに割った場合とで RSS の振る舞いを比べます。考え方を最小のコードで示すと、こうなります。
# Before: 全部を一本のセッションに積む(RSS が右肩上がりになりやすい)
claude -p "$( printf '%s\n' "${ ALL_JOBS [ @ ]}")" --output-format stream-json > run.log 2>&1
# After: セグメントに割り、切れ目でプロセスを終了させる(各セグメント先頭で RSS がリセット)
for batch in "${ SEGMENTS [ @ ]}" ; do
claude -p " $batch " --output-format stream-json > "seg.log" 2>&1
done
私の環境で取れた RSS の傾向は、おおよそ次の通りでした。数値はマシン依存なので絶対値ではなく、形(伸び続けるか、平らに戻るか)を見てください。
指標 Before(単一セッション4本) After(2本×2セグメント)
RSS ピーク 約 690 MB 約 430 MB
終了時 RSS 約 660 MB(高止まり) 各セグメント先頭で約 320 MB に復帰
後半ジョブの所要時間 前半比で目に見えて増加 セグメント間でほぼ一定
OOM 余裕(2 GB 上限想定) ピークで約 34% を消費 ピークで約 21% を消費
劇的な数字を狙う話ではありません。狙いは「ピークを上限から十分離す」ことと「終了時に高止まりさせない」ことです。Before のように終了時まで高止まりすると、同じ VM で次のタスクを連鎖させたときに前の残骸が効いてきます。After のように各セグメント先頭で復帰していれば、何セグメント続けても上限に近づきません。
観測値の読み方と、運用に落とす判断
数字が取れたら、見るべきは三点だけです。
ピークが上限からどれだけ離れているか。2 GB 上限でピークが 1.5 GB なら、スパイク一発で OOM に届く距離です。セグメントを小さくするか、上限を上げる判断材料になります。
セグメント先頭で RSS が復帰しているか。復帰せず累積しているなら、プロセスがちゃんと終了していない(バックグラウンドの子が残っている等)を疑います。私は、この兆候が出たらまず子プロセスの取り残しを確認することを推奨します。
所要時間がセグメント間で一定か。後半だけ伸びているなら、スワップが始まっている兆候です。
運用ループに落とすときは、私はこのアラートを「次のセグメント境界を早める」シグナルとして扱うことを推奨します。けっして実行中のプロセスを強制終了するトリガーにはしないでください。強制終了は部分成果の喪失と、最悪その後始末の不整合を招きます。安全な切れ目で締める、を徹底するほうが、結果として取りこぼしが減ります。
次の一歩
まずは観測だけ始めてください。いま回している無人ジョブに rss-sample.sh を一本足し、一度ぶんの RSS の時系列を CSV に落とすところからで十分です。ピークがどこにあり、終了時に高止まりしているかどうか。それが見えれば、セグメントをどれくらいの粒度で切ればいいかは自然と決まります。アプリ側の省メモリ化に乗りつつ、観測の目を一つ持っておく。これだけで、静かな取りこぼしの多くは防げます。
同じように無人運用で苦労されている方の参考になれば嬉しいです。お読みいただきありがとうございました。