「cache_control を設定したのに、月末の請求書がぜんぜん安くなっていない」— Claude API のプロンプトキャッシュを導入した方から、この相談を一番よく受けます。私自身も最初にキャッシュを入れた時、3日間まったくヒットしていないことに気づかず、「やっぱり効果ないのかな」と諦めかけました。
実は、プロンプトキャッシュが効かない原因は意外と限られています。そして、診断の出発点はリクエストごとに必ず返ってくる usage フィールドにあります。ここでは私が個人運営しているアプリで何度もぶつかった「キャッシュミス」の典型パターンを、診断順に整理します。
まず usage フィールドを読む
Claude API のレスポンスには、必ず usage というオブジェクトが含まれています。プロンプトキャッシュを使っている時は、ここに2つの追加フィールドが現れます。
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model = "claude-sonnet-4-6" ,
max_tokens = 1024 ,
system = [
{
"type" : "text" ,
"text" : "あなたは経験豊富な技術ライターです。" * 200 , # 約2,000トークン
"cache_control" : { "type" : "ephemeral" },
}
],
messages = [{ "role" : "user" , "content" : "こんにちは" }],
)
print (response.usage)
# Usage(
# input_tokens=10,
# cache_creation_input_tokens=2103,
# cache_read_input_tokens=0,
# output_tokens=42
# )
注目するのは次の3つの値です。
input_tokens: キャッシュの対象外で、毎回読み込まれる入力トークン数
cache_creation_input_tokens: 今回のリクエストでキャッシュに「書き込まれた」トークン数(5分TTLなら標準単価の1.25倍、1時間TTLなら2倍が課金されます)
cache_read_input_tokens: キャッシュから「読み込まれた」トークン数(標準単価の0.1倍)
ヒットしている状態とは、2回目以降のリクエストで cache_read_input_tokens がゼロより大きく、cache_creation_input_tokens がゼロに近づいている状態を指します。これが何度リクエストしても変わらないなら、何かが間違っています。
原因①: 最小トークン数を満たしていない
Claude のプロンプトキャッシュには、キャッシュ対象として認められる「最小トークン数」が設定されています。2026年4月時点での目安は次のとおりです。
Claude Sonnet 4.6 / Claude Opus 4.6: 1,024 トークン
Claude Haiku 4.5 系: 2,048 トークン
この閾値を下回るプレフィックスに cache_control を付けても、サーバー側でキャッシュは作られず、cache_creation_input_tokens は常にゼロのまま、input_tokens だけが膨らみ続けます。
短いシステムプロンプト(数百トークン程度)に cache_control を付けて「効かない!」と悩むケースは本当に多いです。私が最初にハマったのもこれでした。診断としては、システムプロンプト全体のトークン数を tokenizer で先に数えてみてください。1,024未満なら、キャッシュよりも先に「プロンプト自体を圧縮しない」方向で設計を見直す方が現実的です。
原因②: cache_control の位置が間違っている
プロンプトキャッシュは「cache_control が付いたブロックの末尾までを1つのキャッシュキーとして登録する」仕組みです。つまり、キャッシュしたい静的部分の 最後 に cache_control を置く必要があります。
# ❌ 間違い: 動的部分の後に cache_control を置いている
system = [
{ "type" : "text" , "text" : SYSTEM_INSTRUCTION },
{ "type" : "text" , "text" : f "今日は { today } です" }, # 動的
{ "type" : "text" , "text" : "出力フォーマットは以下のとおり: ..." ,
"cache_control" : { "type" : "ephemeral" }},
]
上のコードでは、今日は{today}です が日付ごとに変わるため、キャッシュキー全体が毎日変わってしまい、ヒットしません。動的な値はキャッシュ対象の 後ろ に置くのが鉄則です。
# ✅ 正しい: 静的部分の末尾に cache_control、動的部分はその後
system = [
{ "type" : "text" , "text" : SYSTEM_INSTRUCTION },
{ "type" : "text" , "text" : "出力フォーマットは以下のとおり: ..." ,
"cache_control" : { "type" : "ephemeral" }},
{ "type" : "text" , "text" : f "今日は { today } です" }, # キャッシュ後の動的ブロック
]
ツール定義についても同様です。tools 配列の最後の要素に cache_control を付けると、その時点までのツール定義群がまとめてキャッシュされます。
原因③: TTL(有効期限)切れ
デフォルトの TTL(Time To Live)は 5分 です。最後にヒットしてから5分以上アクセスが空くと、キャッシュは破棄されます。低頻度のバックグラウンドジョブや、ユーザーの操作間隔が長いチャットボットでは、毎回キャッシュ書き込みが発生してしまい、コスト削減効果がほぼ消えます。
このパターンは、平日昼間だけ使われる業務ツールでよく観測しました。
対策として、2026年に正式提供された 1時間 TTL が有効です。書き込み時のコストは2倍に上がりますが、アクセス頻度の低いワークロードでは全体としては安くなります。
system = [
{
"type" : "text" ,
"text" : LARGE_SYSTEM_PROMPT ,
"cache_control" : { "type" : "ephemeral" , "ttl" : "1h" },
}
]
導入前後で、cache_read_input_tokens の比率が一日を通してどう変化したかをログに残し、効果を必ず検証してください。
原因④: 動的な値がキャッシュ対象に紛れ込んでいる
これは私が一番頻繁にやらかすミスです。「全リクエストで同じ」と思い込んでいた静的部分に、実は微妙に変動する値が混ざっている、というパターンです。
よくある例:
システムプロンプトの先頭に 現在時刻: 2026-04-28T09:00:34Z のようなタイムスタンプを入れている
ユーザーIDをシステムプロンプトに埋め込んでいる
セッションごとのランダムなトラッキング用UUIDが含まれている
改行コード(\n と \r\n)が環境によって混在している
ハッシュ値で検証するのが確実です。
import hashlib
def cache_key_hash (text: str ) -> str :
return hashlib.sha256(text.encode( "utf-8" )).hexdigest()[: 16 ]
# リクエスト前に、キャッシュ対象部分のハッシュをログ出力
prefix = "" .join(b[ "text" ] for b in system_blocks_until_cache_control)
print ( f "cache prefix hash: { cache_key_hash(prefix) } " )
このハッシュが毎回変わっていたら、何かが動的になっています。差分を取って原因を特定してください。私はこれで「サーバーのリクエスト時刻をうっかり混ぜていた」ことに気づき、半日以上の遠回りを避けられました。
原因⑤: ツール定義のリクエスト順が一定でない
ツール(tools 配列)をキャッシュしているのに効かない時は、配列の順序が原因のことがあります。Claude のキャッシュキーは「内容のバイト列」として比較されるため、ツールの並び順が変わると別のキャッシュとして扱われます。
Python の dict から動的にツール一覧を生成している場合、辞書の挿入順が条件によって入れ替わると、毎回別のキャッシュが作られます。生成ロジックを次のように固定するのが安全です。
TOOL_ORDER = [ "search_docs" , "execute_sql" , "send_email" ]
def build_tools (enabled: set[ str ]) -> list[ dict ]:
# 必ず固定順で並べる
return [ TOOL_DEFINITIONS [name] for name in TOOL_ORDER if name in enabled]
「ユーザーごとに使えるツール集合が違う」設計の場合は、そもそもキャッシュヒット率が出にくくなります。共通ツール群と差分ツールを別ブロックに分け、共通部分だけキャッシュ対象にする設計が現実的です。
診断スクリプト:3リクエスト連続で usage を観察する
原因を切り分ける時に私が使っている、シンプルな診断スクリプトを置いておきます。同じプロンプトを3回連続で投げ、usage の推移を見るだけのものです。
import anthropic
from anthropic.types import TextBlockParam
client = anthropic.Anthropic()
SYSTEM_PROMPT = open ( "system_prompt.txt" ).read() # 1,024 トークン以上を想定
def diagnose ():
for i in range ( 3 ):
resp = client.messages.create(
model = "claude-sonnet-4-6" ,
max_tokens = 64 ,
system = [
TextBlockParam(
type = "text" ,
text = SYSTEM_PROMPT ,
cache_control = { "type" : "ephemeral" },
)
],
messages = [{ "role" : "user" , "content" : f "テスト { i } " }],
)
u = resp.usage
print (
f "req { i } : input= { u.input_tokens } "
f "create= { u.cache_creation_input_tokens } "
f "read= { u.cache_read_input_tokens } "
)
diagnose()
期待される出力:
req 0: input=10 create=2103 read=0
req 1: input=10 create=0 read=2103
req 2: input=10 create=0 read=2103
req 1 以降で read がプラスに転じれば、キャッシュは正しく機能しています。req 1 でも read=0 のままなら、上記の原因①〜⑤を順に潰してください。
より深いコスト最適化の設計パターンについては、Claude API のプロンプトキャッシュで月額コストを半分にした実装メモ と Haiku 4.5・ストリーミング・プロンプトキャッシングを組み合わせて個人開発アプリのAPIコストを抑えた記録 を併せてご覧ください。レート制限と組み合わせた本番設計は API レート制限のベストプラクティス も参考になります。
キャッシュが効いている時にダッシュボードで見える兆候
正しく動き始めると、運用ダッシュボードには3つの変化が現れます。
ひとつ目は、キャッシュ対象プレフィックスの 1リクエストあたり入力トークンコスト がおよそ10分の1に落ちることです。TTLが切れるタイミングで一時的に書き込みコストが乗りますが、グラフ全体の形は「ずっと一定の高さ」から「平らで、たまに小さな山が立つ」形に変わります。
ふたつ目は、cache_read_input_tokens と cache_creation_input_tokens の比率です。10:1 以上なら定常状態として健全です。3:1 を下回るようなら、書き込みコストが節約分を食い潰している状態で、原因③(TTL)か原因④(プレフィックスに動的値が混入)を疑います。
みっつ目は、レイテンシの改善です。キャッシュヒットしたプレフィックスでは、Time to First Token が体感で30〜50%短くなります。これはあまり語られない副次効果ですが、ユーザーが応答を待つ画面を持つプロダクトでは、コスト指標と同じくらい重要なUX指標になります。
この3つを既存のエラー率・レート制限ヘッドルームと同じダッシュボードに並べておくと、デグレが起きた週のうちに気づけるようになります。請求書が来てから慌てずに済むのは、本当に大きな違いです。
実際に効果を測った例:画像メタデータ生成キュー
個人開発で運用しているアプリのバックエンドに、Claude API を使った画像メタデータ生成のキュー処理があります。1枚ごとに約3,200トークンの共通システムプロンプト(分類ルールと出力フォーマットの定義)を毎回送っており、キャッシュ導入前は入力トークンだけで処理コストの大半を占めていました。
導入前後で、同じ1,000枚のバッチを流した時の usage を集計した結果が次のとおりです。
指標 導入前 導入後(5分TTL)
平均 input_tokens / 枚 3,210 12
平均 cache_read_input_tokens / 枚 0 3,198
入力側の実効単価 1.00倍 約0.12倍
Time to First Token(中央値) 1.42秒 0.83秒
見落としがちなのが、キューの投入間隔です。最初は1枚処理するたびに数十秒の外部I/O待ちを挟んでおり、5分TTLでもぎりぎり生き残っていました。ところが夜間バッチで投入間隔が10分近くに開いた時間帯だけ、cache_creation_input_tokens が跳ね上がっていたのです。原因③そのものでした。投入をまとめてバースト実行に変え、間隔を5分以内に収めたところ、cache_read の比率が全時間帯で 10対1 を超えて安定しました。
数値で押さえておくと安心なのは、「read対create が 10対1 を超えているか」の一点です。私自身、感覚で「効いているはず」と思い込んでいた時期の実データを見返すと、実は 4対1 程度で書き込みコストに削られていた、ということが何度もありました。指標を出しておくだけで、この思い込みを避けられます。
TTL をどちらにするかの判断表
5分TTLと1時間TTLのどちらを選ぶかは、アクセス間隔とバースト性で決まります。私が実際の切り分けに使っている目安です。
ワークロードの性質 推奨TTL 理由
チャットUIでの連続対話(数秒〜1分間隔) 5分 書き込み割増が1.25倍で最小。ヒットが途切れない
数分おきのバースト処理 5分+バースト集約 投入間隔を5分以内にまとめれば1時間TTLは不要
数十分間隔の低頻度ジョブ 1時間 書き込み2倍でも生存期間が長く、総額では安い
1日に数回だけ使う業務ツール 1時間+計測 それでも比率が3対1未満ならキャッシュ非対象も検討
迷ったら、まず5分TTLのまま24時間分の read対create 比率を記録し、比率が時間帯で大きく割れているワークロードだけを1時間TTLに切り替えるのが安全です。全部を一律に1時間へ倒すと、高頻度帯では割増分だけ損をします。
診断を最短で回すチェックリスト
キャッシュミスに気づいたら、次の順で潰すのが最短です。上から順に、ひとつずつ usage を見ながら確認してください。
キャッシュ対象プレフィックスのトークン数を数え、モデルの最小閾値(Sonnet系1,024/Haiku系2,048)を超えているか確認する(原因①)
cache_control が静的部分の末尾ブロックに付いているか、動的ブロックがその後ろにあるかを確認する(原因②)
アクセス間隔がTTL(既定5分)を超えていないか、ログのタイムスタンプで確認する(原因③)
プレフィックスのSHA-256ハッシュをリクエストごとに出力し、値が一定かを確認する(原因④)
ツール配列の並び順が固定されているか、生成ロジックを確認する(原因⑤)
この5項目を1度チェックすれば、私が本番でぶつかったキャッシュミスはほぼ説明がつきました。原因は複合していることもあるため、ひとつ直して改善しなければ、次の項目へ進んでください。
次の一歩
まずは手元で動かしているプロダクトに上の診断スクリプトを2分で組み込み、cache_read_input_tokens が増えるかどうかを目視で確認してみてください。増えていなければ、原因①「最小トークン数」から順に潰すのが最短ルートです。
キャッシュは「設定すれば効く」機能ではなく、「効いていることを usage で観測しながら設計を直す」機能です。診断ループを一度回せるようになると、本番の請求書は確実に小さくなります。