バッチ処理でプロンプトキャッシュのヒット率を上げる方法
Message Batches APIとプロンプトキャッシュを併用する際のcache_control設計と、ヒット率30〜98%の幅が生まれる理由を実装例つきで解説します。
このTipsでできること
Message Batches APIはリクエストを非同期・並列に処理するため、プロンプトキャッシュのヒットは保証されず「ベストエフォート」扱いになります。公式ガイドは、トラフィックのパターンによってヒット率が30%〜98%まで幅を持つと明記しています。この記事では、ヒット率を高く保つcache_controlの書き方、先行リクエストで書き込みを済ませる2段階送信、ヒット率が低いときの切り分けを扱います。
バッチとキャッシュの基本数字
バッチの料金
50%
通常のAPI料金に対する割合
ヒット率の幅
30〜98%
トラフィックのパターンで変わる
キャッシュ読み込み
0.1倍
既定の倍率。Claude Opus 5.5とSonnet 5.5は0.05倍、Fable 5.1とMythos 5.1は0.025倍
キャッシュの有効期間
5分 / 1時間
既定は5分。1時間は書き込み料金が高い
並列処理でヒット率が揺れる仕組みと基本の書き方
Batch APIは、Messagesリクエストを最大10万件または256MBまで(どちらか先に達したほう)まとめて送信し、非同期で処理する仕組みです。多くのバッチは1時間以内に完了します。バッチは24時間以内に処理が終わらないと期限切れになり、結果は作成から29日間ダウンロードできます。
ヒット率が保証されない理由は、バッチのリクエストが並列かつ非同期に処理される点にあります。通常の同期APIなら順番に送れるので、キャッシュの書き込みと読み込みの順序を制御しやすくなります。バッチでは複数のリクエストが同時に走るため、書き込みが終わる前に別のリクエストが同じ内容を処理することがあります。結果も作成順に返るとは限らないので、突き合わせにはcustom_idを使います。
公式ガイドが挙げるヒット率向上の3点は次のとおりです。
- バッチ内のすべてのMessageリクエストに、同一の
cache_controlブロックを含める - リクエストを途切れさせず一定のペースで送り続ける(キャッシュエントリは5分で失効するため)
- リクエスト間でできるだけ多くのキャッシュ対象コンテンツを共有する
バッチの処理は5分を超えることがあります。そのため、キャッシュの有効期間は通常の5分と1時間のどちらかを、料金と引き換えに選ぶことになります。
バッチで使うキャッシュの有効期間
5分キャッシュ
書き込みは通常入力の1.25倍です。バッチの処理が5分を超えると、処理中にキャッシュが失効しうるのが弱点です。
1時間キャッシュ
書き込みは通常入力の2倍で、cache_controlにttlを指定します。バッチは5分から1時間かかるのが一般的なため、公式ガイドは5分キャッシュより向くと案内しています。
以下は、同じシステムプロンプトと長文コンテキスト(『高慢と偏見』全文)をcache_control付きで2つのリクエストに含め、バッチとして送信する例です。
curl https://api.anthropic.com/v1/messages/batches \
--header "x-api-key: $ANTHROPIC_API_KEY" \
--header "anthropic-version: 2023-06-01" \
--header "content-type: application/json" \
--data \
'{
"requests": [
{
"custom_id": "my-first-request",
"params": {
"model": "claude-opus-5",
"max_tokens": 1024,
"system": [
{
"type": "text",
"text": "You are an AI assistant tasked with analyzing literary works.\n"
},
{
"type": "text",
"text": "<the entire contents of Pride and Prejudice>",
"cache_control": {"type": "ephemeral"}
}
],
"messages": [
{"role": "user", "content": "Analyze the major themes in Pride and Prejudice."}
]
}
},
{
"custom_id": "my-second-request",
"params": {
"model": "claude-opus-5",
"max_tokens": 1024,
"system": [
{
"type": "text",
"text": "You are an AI assistant tasked with analyzing literary works.\n"
},
{
"type": "text",
"text": "<the entire contents of Pride and Prejudice>",
"cache_control": {"type": "ephemeral"}
}
],
"messages": [
{"role": "user", "content": "Write a summary of Pride and Prejudice."}
]
}
}
]
}'2つのリクエストでsystem配列の内容とcache_controlブロックが完全に一致している点がポイントです。2番目のリクエストの処理時点で1番目のキャッシュ書き込みが終わっていれば、長文コンテキスト分のトークンはキャッシュから読み込まれ、課金と処理時間の両方が減ります。messagesの質問文はリクエストごとに変わってかまいません。cache_controlの対象をsystem側に限っていれば、質問文の違いはヒット判定に影響しません。
この例の{"type": "ephemeral"}は5分キャッシュです。1時間キャッシュにするには、同じブロックを次のように書き換えます。
"cache_control": {"type": "ephemeral", "ttl": "1h"}先行リクエストで書き込みを済ませる2段階送信
並列処理の競合を避ける方法として、公式ガイドは共通プレフィックスの書き込みだけを先に済ませる手順を案内しています。プロンプトキャッシュのFAQでは、1時間キャッシュを使うこのやり方が最もコスト効率が良いとされています。
2段階送信の流れ
- 1
共通プレフィックスを洗い出す
システムプロンプトや長文コンテキストなど、全リクエストで同じ部分を決めます。
- 2
先行バッチを1件だけ送る
共通プレフィックスと1時間キャッシュのブロックだけを持つリクエスト1件を、単独のバッチとして送ります。
- 3
完了を待つ
先行バッチの
processing_statusがendedになるまで状態を確認します。 - 4
本体をまとめて送る
残りのリクエストを、同じ共通プレフィックスと
cache_controlで送ります。
先行方式では、書き込み料金(1時間キャッシュは通常入力の2倍)を先行の1件で済ませておけます。本体のリクエストは、ヒットすれば読み込みの倍率で処理され、書き込みを避けやすくなります。ヒットはベストエフォートなので、本体側で書き込みが起きることもあります。
先行リクエストは出力が不要なので、max_tokensは小さくしておけます。バッチでは1以上が必須で、max_tokens: 0によるキャッシュの事前ウォームアップは使えません。バッチ処理中に書いた一時キャッシュが後続のリクエストより先に失効しうるためです。先行リクエストの例は次のとおりです。
{
"custom_id": "prime-prefix",
"params": {
"model": "claude-opus-5",
"max_tokens": 1,
"system": [
{
"type": "text",
"text": "<共通のシステムプロンプトと長文コンテキスト>",
"cache_control": {"type": "ephemeral", "ttl": "1h"}
}
],
"messages": [{"role": "user", "content": "ok"}]
}
}完了の確認は、バッチのprocessing_statusをポーリングします。
import time
import anthropic
client = anthropic.Anthropic()
message_batch = None
while True:
message_batch = client.messages.batches.retrieve(MESSAGE_BATCH_ID)
if message_batch.processing_status == "ended":
break
print(f"Batch {MESSAGE_BATCH_ID} is still processing...")
time.sleep(60)processing_statusは作成直後がin_progressで、全リクエストの処理が終わるとendedに変わります。コンソール画面でも状態は見られますが、自動化するパイプラインではAPI経由のポーリングが基本です。
バッチではほかにもstream: trueとspeed(Fast mode)が使えず、含めるとバリデーションエラーになります。結果は1つのファイルで返り、Fast modeは同期処理の待ち時間を縮める機能だからです。
ヒット率が低いときの切り分け
ヒット率が低いとき、まず見るのは結果に付くusageです。バッチの結果は1行1リクエストのJSONLで、各行のmessage.usageにcache_creation_input_tokens(書き込み)とcache_read_input_tokens(読み込み)が入ります。次の表は、その値の出方から原因を絞る早見表です。
| 症状 | 疑う点 | 対処 |
|---|---|---|
| 書き込みも読み込みも0 | 疑う点共通部分がモデルごとの最小長に届いていない | 対処共通部分を増やす、またはモデルを見直す |
| 毎回書き込みだけで読み込みが0 | 疑う点breakpointが毎回変わるブロックにある | 対処cache_controlを静的部分の末尾へ移す |
| 一部だけ読み込み、残りは書き込み | 疑う点書き込み完了前に別リクエストが処理された | 対処先行バッチ方式にする |
| 時間が経つほど読み込みが減る | 疑う点5分キャッシュの失効 | 対処1時間キャッシュにする、または途切れず送る |
1行目の最小長は、プロンプトキャッシュのページにモデル別の値があります。Claude APIでは、Claude Opus 5.5・Opus 5・Sonnet 5.5などは512トークンです。Sonnet 5・Sonnet 4.6・Opus 4.8などは1,024トークン、Opus 4.7は2,048トークンです。Opus 4.6・Opus 4.5・Haiku 4.5は4,096トークンになります。最小長に届かないと、エラーは返らずキャッシュなしで処理されるため、usageの値でしか気づけません。
2行目は見落としやすい型です。公式ガイドは、キャッシュの探索が過去に書かれたエントリしか見つけられず、書き込みはbreakpointの位置でしか起きないと説明しています。リクエストごとに変わるタイムスタンプや質問文にbreakpointを置くと、毎回新しいエントリを書くだけで読み込みが起きません。cache_controlは、全リクエストで同一になる最後のブロックに置きます。
ヒット率そのものは、結果のJSONLから自分で出せます。公式ガイドの30%〜98%は単位(リクエスト数かトークン量か)が示されていないため、両方を出して見比べると比較しやすくなります。
import json, sys
ok = hit = read = total = 0
for line in open(sys.argv[1]):
r = json.loads(line)
if r["result"]["type"] != "succeeded":
continue
u = r["result"]["message"]["usage"]
cr = u.get("cache_read_input_tokens", 0)
ok += 1
hit += cr > 0
read += cr
total += cr + u.get("cache_creation_input_tokens", 0) + u["input_tokens"]
print(f"リクエスト基準 {hit}/{ok} ({hit / ok:.0%})")
print(f"トークン基準 {read}/{total} ({read / total:.0%})")エラーや期限切れの結果はsucceededではないため、このスクリプトは数えません。結果は作成から29日で取得できなくなるので、記録を残すなら期限内に集計します。
入力トークンの合計は、読み込み・書き込み・キャッシュ対象外の入力を足した値です。書き込み2件と読み込み1件の3リクエスト(ほかにエラー1件)を模したJSONLでは、読み込み分は5,000トークンです。成功した3件の入力合計が15,067トークンなので、リクエスト基準もトークン基準も33%と出ます。
バッチリクエストを共有プレフィックスでグルーピングする
ヒット率を実務で上げる鍵は、1つのバッチに詰めるリクエストのグルーピングです。10万件を雑多に詰め込むより、同じシステムプロンプトと長文コンテキストを使うリクエストごとに別バッチへ分けたほうが、共有プレフィックスの割合が上がってヒット率も安定します。
バッチに入れられるリクエストの種類は幅広くなっています。Vision(画像入力)、サーバーツール(web検索・web取得・コード実行・MCPコネクタ・advisor・tool search)を含むツール使用、システムメッセージ、マルチターンの会話、拡張思考(extended thinking)、ほとんどのベータ機能が対象です。稼働中のすべてのモデルが使えます。種類の違うリクエストを同じバッチに混ぜることもできます。ただしヒット率の面では、共有プレフィックスの内容が同一であることが条件のままです。
マルチターンの会話をバッチに入れるときは、探索の範囲にも注意が要ります。キャッシュの探索はbreakpointから最大20ブロックさかのぼるだけです。前回の書き込み位置から20ブロック以上離れると、一致するエントリを見つけられません。ターンが長く伸びる会話では、書き込み位置の近くに2つ目のbreakpointを置いておきます。
cache_controlブロックの値だけ揃えても足りない場合があります。キャッシュはプロンプトの階層(tools → system → messages)を順に追う仕組みで、ある階層の内容が変わると、それ以降のキャッシュがまとめて無効になります。ツール定義やシステムプロンプトの文言が1文字でも違えば、cache_controlブロックが同じでも別エントリです。共有するsystem配列やツール定義はテンプレート化し、変数展開ではなく同じ文字列を全リクエストにコピーする実装にしておくと事故が減ります。
料金の重ね掛けとワークロード別の効き方
プロンプトキャッシュの倍率は、バッチAPIの割引やデータレジデンシーといった他の料金修飾子と重ね掛けされます。バッチの50%引きはベースの入力・出力単価に適用され、その上でキャッシュ読み込み分がさらに下がる計算です。共通のシステムプロンプトや長文リファレンスを扱うワークロード(大量ドキュメントの一括分析、評価データセットの処理など)ほど、この組み合わせの効果が大きくなります。
プロンプトキャッシングの仕組みと課金の考え方はAnthropic APIのPrompt Cachingを理解するにあります。バッチAPI自体の使い方はClaude Batch APIの使い方が扱っています。
よくある質問
バッチのキャッシュはワークスペースをまたいで共有されるか
共有されません。Claude API・Claude Platform on AWS・Microsoft Foundryでは、キャッシュは組織内でもワークスペースごとに分離されます。バッチもワークスペース単位で隔離されるため、別ワークスペースのバッチが同じキャッシュを読むことはありません。キャッシュキーはブレークポイントまでのプロンプト内容から作る暗号学的ハッシュで、同一のプロンプトだけが同じキャッシュに届きます。Amazon BedrockとGoogle Cloudは、組織単位の分離です。ワークスペースを複数使う構成では、キャッシュ戦略への影響を見ておく必要があります。