Max Nardit
← 記事
15 mindev

Claude Code のバックグラウンドサブエージェントと利用上限:ステータスラインが99%安くなるという Anthropic の主張と、自分で測れるキャッシュ書き込み

サブエージェントの要は分離にあり、自分のトランスクリプトでは、そのコストの大半が5分のキャッシュ書き込みでした。Max プランで計測すると、バックグラウンドサブエージェントは価格換算した使用量の約11.5%で、その64%がキャッシュ書き込みです。再現用の Python スクリプトと、4つのレバーがそれぞれコストのどの部分に効くかをまとめます。

目次

11月4日、Anthropic の開発者向けアカウントが、Claude Code のバックグラウンドサブエージェントが利用上限を消費する量が減ったと投稿しました。ステータス更新1回あたりのコストが「約99%安く」なったからだそうです。同じ日に Claude Code 2.1.318 がリリースされ、バックグラウンドサブエージェント向けの変更がさらに3つ入りました。自分の課金にどう効くのかを知りたくて、2週間分のトランスクリプトを調べました。Anthropic が安くなったと言うリクエストは、どこにも見つかりませんでした。見えたのはサブエージェント自身の作業で、そのコストの3分の2近くが5分のキャッシュ書き込みです。ステータスラインの変更は別のリクエストの話で、2.1.318 のキャッシュ修正はその書き込みの発生源の1つを狙ったものですが、大まかな代理指標で見る限り、自分のログではその発生源は小さいものでした。

以下の数字は、ファーストパーティ API 上の Max サブスクリプションで、10月22日から11月4日までのものです。期間は 2.1.318 がリリースされた日で終わるので、これはベースラインであって、前後比較ではありません。

Claude Code 2.1.316〜2.1.318 はバックグラウンドサブエージェントの何を変えたのか

通知の修正、キャッシュの修正、新しい effort 設定で、3つのリリースに分かれています。2.1.318 のリリースノートでは、「Now using usage credits」の通知が「バックグラウンドサブエージェントの実行中、約30秒ごとに」再表示される問題が修正されました。バックグラウンドサブエージェントのシェルコマンドは、3分たつとバックグラウンドへ移されるようになりました。これは「すべてのプロバイダーで、実行中にプロンプトキャッシュが切れないように」するためです。さらに Bedrock、Vertex、Foundry、またはテレメトリーを無効にした環境では、プロンプトキャッシュが冷えているとき、サブエージェントの進捗サマリーが「サブエージェントの保留中のレスポンスを最大5分待てるように」なりました。

2.1.316 では CLAUDE_CODE_SUBAGENT_EFFORT が加わりました。サブエージェントが動く effort レベルで、「ほかに何も指定がないとき、セッションのレベルを引き継ぐ代わりに」使われます。2.1.317 では、メインの会話と別のモデルで動くサブエージェントが「そのモデルに保存された effort レベル、またはそのデフォルト」を使うようになり、Claude apps gateway の管理 API にコストレポート(GET /v1/organizations/analytics/cost)が追加されました。過去の支出を開発者別、日別、グループ別に分けて見られます。

3つのリリースノートのどれも、ステータスラインの変更には触れていません。この変更が出てくるのは Anthropic の投稿だけで、投稿はそれを v2.1.316 以降に結びつけています。

Claude Code のサブエージェントのステータス更新について Anthropic は何と言っているか

ステータスラインは以前、30秒ごとにサブエージェントの会話全体を読み直していたが、今はそうしない、という主張です。最初の投稿は次の通りです。

Background subagents in Claude Code now use less of your usage limit.
A running subagent updates its status line every 30 seconds, and each
update used to re-read its whole conversation. Now Haiku 5.5 writes it
from short notes instead, so each update costs about 99% less.

続報では、セッション単位の数字が示されています。「バックグラウンドサブエージェントを使うセッションでは利用上限の消費が約5%減ったので、同じ上限で約6%多くプロンプトを送れる」。さらに、この変更は「Pro、Max、Ultra のほとんどの利用者と、Team と Enterprise の全員に提供済み」だとしています。

3つの数字はどれも Anthropic が自社の利用者全体で測ったもので、手法は公開されていません。「ほとんどの利用者に提供済み」というのは段階的な展開のことなので、個々のアカウントに届いているかどうかはわかりません。2.1.318 の Bedrock、Vertex、Foundry、テレメトリー無効の環境についての記述からは、そうした環境では進捗サマリーがいまもサブエージェント自身のキャッシュを待っていることがうかがえます。リリースノートに書かれているわけではなく私の見立てですが、安い経路はファーストパーティの機能です。

自分のログでも確認できませんでした。~/.claude/projects/<project>/<session-id>/subagents/ にあるサブエージェントのトランスクリプトには、サブエージェントのリクエストがモデル、effort、トークン使用量とともに記録されています。対象期間には 2.1.316 以降で数百件のバックグラウンドサブエージェントのリクエストが動いていましたが、Haiku のリクエストを記録したサブエージェントのファイルは1つもありません。セッションファイルにある Haiku のリクエストは、自分で Haiku を使って動かしたセッションのものだけです。展開がこのアカウントにまだ届いていなかったのか、ステータスラインのリクエストが自分の見た場所に書かれないのか、どちらなのかはわかりません。そのため、この調査ではそのリクエストを特定できず、5%を裏づけることも否定することもできません。

Claude Code のサブエージェントのプロンプトキャッシュが5分なのはなぜか

Claude Code がデフォルトで1時間のキャッシュを与えるのは、メインの会話といくつかの補助リクエストだけだからです。プロンプトキャッシュのドキュメントは、すべてのリクエストを2つの区分のどちらかに入れています。サブスクリプションでプランの利用枠内なら、メインの会話は1時間です。それ以外、つまりサブエージェント、ワークフロー、チームメイト、フォーク、コンパクションは5分で、Anthropic が1時間に設定しているサーバー制御の補助リクエストだけが例外です。サブエージェントは「サブスクリプションでも、長いものを自分で選ぶまでは5分」です。

2.1.318 の3分ルールの理由はここにあります。ビルドやテストスイートを5分以上待ったサブエージェントは、期限切れのキャッシュに戻ることになり、次のリクエストでコンテキストをキャッシュ書き込みの料金でもう一度書き込みます。3分の時点でコマンドをバックグラウンドへ移すのは、それを防ぐためです。

ドキュメントにはさらに、サブエージェントは独自の指示とツールセットを持つ別の会話を始めるので、最初のリクエストは「2つのプレフィックスが異なるため、親のキャッシュを読まない」とあります。例外はフォークで、親のプレフィックスを引き継ぎ、最初のリクエストで親のキャッシュを読みます。

メインの会話への影響もあります。サブスクリプションがプランの上限を超えて利用クレジットを使い始めると、promptCacheTtl を固定していない限り、Claude Code はメインの会話も5分の TTL に下げます。「Now using usage credits」の通知が示しているのはこの瞬間です。5分の書き込みはトークンあたりでは安いものの、5分以上アイドルになったメインの会話は次のプロンプトでキャッシュを作り直すことになり、クレジット利用中はその作り直しが課金されます。クレジットに切り替わるとアイドル時コンパクションが止まるのも、この同じ切り替えのせいです。

Claude Code のバックグラウンドサブエージェントのコストを測る方法

まず /usage を見て、それからトランスクリプトを読みます。Pro、Max、Ultra、Team、Enterprise プランでは、/usage の内訳が、直近の使用量をスキル、サブエージェント、プラグイン、MCP サーバーごとに、過去24時間または7日間の割合として示します。計算元はローカルの履歴です。これで割合はわかりますが、原因はわかりません。同じ画面の Prompt cache (main) の行は「メインの会話だけが対象で、サブエージェントは含まない」ので、キャッシュの挙動を見るにはファイルが必要です。

トランスクリプトの assistant の各行には、usage オブジェクト(TTL 別のキャッシュ書き込み、キャッシュ読み取り、出力)、model、effort、Claude Code の version が入っています。各サブエージェントファイルの隣には .meta.json があり、バックグラウンドで動いたかどうか、フォークだったかどうかがわかります。下のスクリプトは、指定した期間を区分ごとに合計し、キャッシュの価格表にある Opus 5.5 の定価で重み付けします。100万トークンあたり、入力 $4、5分の書き込み $5、1時間の書き込み $8、キャッシュ読み取り $0.20、出力 $20 です。サブスクリプションでは、これは重み付けであって請求額ではありません。トークンがプランの使用量にどう換算されるかを、Anthropic は公表していません。

python
# subagent_cost.py: python3 -I subagent_cost.py 2026-10-22 2026-11-05
import collections, glob, json, os, sys
from datetime import datetime, timezone
 
PRICE = dict(inp=4, w5=5, w1h=8, read=0.20, out=20)  # Opus 5.5 list, $ per MTok
start, end = (datetime.fromisoformat(d).replace(tzinfo=timezone.utc) for d in sys.argv[1:3])
seen, tok, info = set(), collections.defaultdict(collections.Counter), collections.Counter()
 
def bucket_of(path):
    if "/subagents/" not in path:
        return "main"
    try:
        meta = json.load(open(path[:-6] + ".meta.json"))
    except (OSError, ValueError):
        meta = {}
    if meta.get("agentType") == "fork":
        return "fork"
    return "sub-" + meta.get("requestShape", "unknown")  # sub-background / sub-foreground
 
def parts(u):
    # A request with an advisor call lists each model call under "iterations";
    # the top-level TTL split then covers only the first one.
    its = u.get("iterations") or [dict(u, type="message")]
    for it in its:
        cc = it.get("cache_creation") or {}
        yield it.get("type"), dict(
            inp=it.get("input_tokens") or 0, read=it.get("cache_read_input_tokens") or 0,
            w5=cc.get("ephemeral_5m_input_tokens") or 0, w1h=cc.get("ephemeral_1h_input_tokens") or 0,
            out=it.get("output_tokens") or 0, writes=it.get("cache_creation_input_tokens") or 0)
 
paths = sorted(glob.glob(os.path.expanduser("~/.claude/projects/**/*.jsonl"), recursive=True))
for path in paths:
    b, first, prev = bucket_of(path), True, None
    for line in open(path, errors="ignore"):
        try:
            rec = json.loads(line)
            msg, u = rec["message"], rec["message"]["usage"]
            ts = datetime.fromisoformat(rec["timestamp"].replace("Z", "+00:00"))
        except (ValueError, KeyError, TypeError):
            continue
        is_first, gap = first, prev is not None and (ts - prev).total_seconds() > 300
        first, prev = False, ts  # copied history still sets the order
        if not msg.get("id"):
            info[(b, "skipped: no message id")] += 1
            continue
        if msg["id"] in seen:  # streamed chunks and resumed sessions repeat a message
            continue
        seen.add(msg["id"])
        if not start <= ts < end:
            continue
        for kind, p in parts(u):
            t = tok[b if kind == "message" else "advisor (other model, not priced)"]
            t.update(p)
            t["w_unsplit"] += p["writes"] - p["w5"] - p["w1h"]
            t["w_first"] += p["writes"] if is_first else 0
            t["w_after_gap"] += p["writes"] if gap else 0
        info[(b, rec.get("version"), msg.get("model"), rec.get("effort"))] += 1
 
priced = {b: sum(t[k] * PRICE[k] / 1e6 for k in PRICE) for b, t in tok.items() if "advisor" not in b}
grand = sum(priced.values()) or 1
for b, t in sorted(tok.items()):
    w = max(t["writes"], 1)
    print(f"\n{b}: " + (f"{100 * priced[b] / grand:.1f}% of priced usage" if b in priced else f"{t['inp'] + t['out']} tokens"))
    if b in priced and priced[b]:
        print("  split", {k: f"{100 * t[k] * PRICE[k] / 1e6 / priced[b]:.0f}%" for k in PRICE if t[k]})
    print(f"  cache-read tokens per cache-write token: {t['read'] / w:.1f}")
    print(f"  writes on a file's first request: {100 * t['w_first'] / w:.0f}%")
    print(f"  writes after a >5 min gap: {100 * t['w_after_gap'] / w:.2f}%")
    print(f"  writes with no TTL split: {t['w_unsplit']}")
for key, n in sorted(info.items(), key=str):
    print(*key, n)

正しく扱うべき点が2つあります。1つ目は、メッセージ ID での重複排除をファイルごとではなく、全ファイルを通して行うことです。ストリーミングされた返答は同じ usage を複数の行に書き、再開したセッションは以前のメッセージを新しいファイルにコピーします。最初の集計ではこの両方を間違え、メインの会話を大幅に多く数えていました。2つ目は、ファイルの更新日時ではなく、各レコードの timestamp で絞り込むことです。そうしないと、長く動いているセッションが何週間も前のリクエストを期間内に持ち込みます。一部のリクエストには、usage.iterations の下に2つ目のモデルへの advisor 呼び出しも含まれます。スクリプトは iteration ごとに TTL の内訳を取り、advisor のトークンは別のモデルの料金で課金されるので、価格を付けずに別の行に出します。主に別のモデルを使っているなら、価格表のそのモデルの行に差し替えてください。

Claude Code のサブエージェントのトランスクリプト2週間分からわかったこと

バックグラウンドサブエージェントは価格換算した使用量の約9分の1で、そのコストの3分の2近くがキャッシュ書き込みでした。

10月22日〜11月4日、Opus 5.5 の定価で換算メインの会話バックグラウンドサブエージェントフォーク
価格換算した使用量に占める割合87.4%11.5%1.0%
キャッシュ書き込み(区分内の割合)52%(1時間)64%(5分)63%(5分)
キャッシュ読み取り(区分内の割合)39%24%29%
出力(区分内の割合)9%12%8%
キャッシュ書き込み1トークンあたりのキャッシュ読み取りトークン29.69.411.7

フォアグラウンドのサブエージェントは0.1%で、何かを読み取るには少なすぎます。2つ目のモデルへの advisor 呼び出しは、すべての割合から除いています。バックグラウンドサブエージェントでは、キャッシュ書き込み1トークンにつきキャッシュ読み取りが約9トークン、メインの会話では約30トークンでした。Opus 5.5 では、キャッシュ読み取りは基本入力の0.05倍、5分の書き込みは1.25倍で、その差は25倍です。つまり読み取りが書き込みの25倍に届かなければ、書き込みのほうが大きな項目になります。

その書き込みの大半は、最初のリクエストから来ているわけではありません。最初のリクエストを合わせても、バックグラウンドサブエージェントのキャッシュ書き込みの10%です。出力でもあまり説明できません。出力はコストの12%で、$20 で換算されているので、仮にすべての出力トークンが $5 で1回ずつ書き戻されたとしても、64ポイントのうち約3ポイントにしかなりません。残りの85%ほどは、後のターンでキャッシュに入ったそれ以外のすべてです。ツールの結果、指示、無効化後の作り直しなどですが、トランスクリプトにはどれなのかが書かれていないので、そのうちどれだけがツール出力なのかは言えません。最初に試す価値があるのはこの部分です。自分で制御できる部分だからです。

長い中断はまれでした。同じサブエージェントの前のリクエストから5分以上たって来たリクエストは、バックグラウンドサブエージェントのキャッシュ書き込みの0.73%でした。これは3分ルールが狙う期限切れの代理指標であって、その計測ではありません。間隔が空いたことはキャッシュが切れた証拠にはならず、それらの書き込みには新しい内容も含まれます。それでも、自分のサブエージェントがこの問題が出てくるほど長くアイドルになることはめったになかった、とは言えます。サブエージェントの中で長いビルドを回すワークロードなら違って見えるはずで、自分がどちらなのかはスクリプトの同じ行でわかります。

effort フィールドは自分の設定と一致しませんでした。settings.json には effortLevel: xhigh とありますが、期間内のバックグラウンドサブエージェントのリクエストはすべて medium で動いていました。理由は2つあります。自分のセッションはコマンドラインの --effort medium で始まり、セッションと同じモデルのサブエージェントはセッションのレベルを引き継ぎます。それにモデル設定のドキュメントによると、ユーザー設定のトップレベルの effortLevel はそもそも「Opus 5.5 には効かない」もので、Opus 5、Fable 5.1 とそれ以前のモデルにまだ適用される古い形式です。実際に動いたレベルは、トランスクリプトの effort フィールドに出ています。

Claude Code のどの設定でバックグラウンドサブエージェントの消費が変わるのか

レバーは4つあり、それぞれコストの別の部分に効きます。ここではどれも計測していません。自分ならまずここから試す、という場所です。

  • サブエージェントが読み込むもの(64%の書き込みの一部)。テストログ全体、行範囲で足りるのにファイル全体、絞り込みのない検索。新しい内容は、後のターンで安く読めるようになる前に、入力の1.25倍の書き込みとしてキャッシュに入ります。対応しているツールには要約を求め、サブエージェントに渡すコマンドでは出力を制限し、関連する作業は1つのサブエージェントにまとめて、同じファイルを2回読み込まないようにします。再開したサブエージェントの最初のリクエストは、元の実行で温めたキャッシュを読めますが、それはキャッシュがまだ生きている間、つまりデフォルトでは5分以内だけです。
  • モデル(すべての列の価格が変わる)。検索や要約には、サブエージェントのフロントマターで model: haiku を指定します。Claude Code はサブエージェントのモデルを、呼び出しごとのパラメーター、フロントマター、CLAUDE_CODE_SUBAGENT_MODEL の順に決めるので、この変数だけではフォールバックにすぎません。Haiku 5.5 のサブエージェントの記事で、effort レベルと 100K の価格ラインを扱っています。どちらも、ほかのどのモデルよりこのモデルで大きく効きます。
  • Effort(出力の12%と、コンテキストに残る思考)。自分で定義するエージェントにはフロントマターで effort: を設定し、それ以外には CLAUDE_CODE_SUBAGENT_EFFORT を使います。今日の時点でこの変数は 2.1.316 の changelog にはありますが、環境変数のページには載っていないので、設定したあとはサブエージェントのトランスクリプトで effort フィールドを確認してください。CLAUDE_CODE_EFFORT_LEVEL はサブエージェントのどの設定よりも優先されます。
  • サブエージェントのキャッシュ TTL(期限切れのケースだけ)。subagentPromptCacheTtl: "1h" または CLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTL=1h で、メインの会話以外の区分全体が1時間になり、FORCE_PROMPT_CACHING_5M=1 はそれを上書きします。1時間の書き込みは $5 に対して $8 です。記録された5分の書き込みを1時間の料金で換算し直すと、区分の64%に60%が上乗せされ、バックグラウンドサブエージェントの行ではおよそ38%増えます。これは避けられる書き込みを差し引く前の数字です。元が取れるのはサブエージェントが5分を超えてアイドルになることが多い場合だけで、自分の場合はめったにありませんでした。

Claude Code のバックグラウンドサブエージェント:見るべき数字は99%なのか

自分の課金については違います。Anthropic の5%は利用者全体のプラン使用量で、1つのアカウントからは再現できません。見られる数字は、自分のトランスクリプトにあります。自分の場合、バックグラウンドサブエージェントは価格換算した使用量の11.5%で、そのキャッシュ書き込みは全体の約7%でした。だから最初に見るべきは書き込みの列です。

その書き込みは、サブエージェントがそもそも何のためにあるのか、その代価でもあります。サブエージェントは、テストログやクロールの結果をメインの会話の外に置いておきます。メインの会話に入れば、次のコンパクションまで後のターンのたびに読み直されるものです。サブエージェントはそれをキャッシュするために払い、そのあとコンテキストを捨てます。このトレードが得かどうかはワークロードしだいで、同じジョブを両方のやり方で動かして測ったことはありません。Pro、Max、Ultra プランで複数のバックグラウンドサブエージェントを動かす前に、一度スクリプトを実行してその列を見てください。そして動かす数を減らす前に、各サブエージェントが読み込むものを削ってください。

01 / ディスカッション

コメント欄はありません。議論は X で行っています。

Max Nardit@mnardit

02 / ほかの記事

  1. Claude Code の worktree: claude --worktree が分離するものと、ポート、データベース、インストールのように分離しないもの

    Claude Code 2.1.316 で検証しました。2 つの worktree が同じポートで衝突し、Docker Compose はそれぞれに別の Postgres ボリュームを計画し、スクリプト実行はロックされたチェックアウトと不要なブランチを残し、デフォルトのベースは未プッシュのコミットを含みませんでした。

  2. Claude Code のメモリを Codex 0.165 にインポートする:最初のインポートより前に memories を有効にしないと、プロファイルは v1 のまま

    ノートのコピー自体は簡単な部分です。Claude Code のファイルはすべてそのまま残り、Codex がセッション開始時に読み込むのは、サイズ上限が厳格に決まったサマリーの中にモデルが書いた、そのファイルへの道筋です。デフォルトでオフのスイッチ 2 つと、紛れ込んだディレクトリ 1 つが、ファイルがどのメモリパイプラインに入るかを決めます。

  3. Vox の AGENTS.md の行を Claude Code で 90 回試した: いちばん効いた行は彼のものではなかった

    1 行目を入れるとエージェントは入力チェックを省きました。2 行目が足した要素は、1 行目と同じファイルに並ぶと消えました。「without breaking anything」は、テストコマンドを書いた行がすでに果たしていた以上の効果をはっきりとは示しませんでした。90 回の実行で言えること、言えないこと、そして行を残す前に試す方法。