◉CLAUDE LABEN
●2.1.301 — vim の % モーション、VS Code パネルでの LaTeX 数式表示、使用量上限待ちのリトライ監視の上限が入りました(10/15)●2.1.299 — 200K トークン超で約54分放置した会話はアイドル圧縮。MCP の許可ルールは名指ししたサーバーだけに一致します●SONNET 4.5 — Claude API での提供終了は 11/30、残り45日。移行先は Sonnet 5.5●WINDOWS — 日本語の環境でだけフックが黙って壊れる報告があり、文字コードの確認が呼びかけられています●FAST MODE — Opus 5.5 の Fast mode がリサーチプレビューを終え、ベータヘッダーが不要になりました(10/15)●NEW — Cowork に渡したフォルダの先頭に、十行の約束ファイルを置いてみました●2.1.301 — vim の % モーション、VS Code パネルでの LaTeX 数式表示、使用量上限待ちのリトライ監視の上限が入りました(10/15)●2.1.299 — 200K トークン超で約54分放置した会話はアイドル圧縮。MCP の許可ルールは名指ししたサーバーだけに一致します●SONNET 4.5 — Claude API での提供終了は 11/30、残り45日。移行先は Sonnet 5.5●WINDOWS — 日本語の環境でだけフックが黙って壊れる報告があり、文字コードの確認が呼びかけられています●FAST MODE — Opus 5.5 の Fast mode がリサーチプレビューを終え、ベータヘッダーが不要になりました(10/15)●NEW — Cowork に渡したフォルダの先頭に、十行の約束ファイルを置いてみました
記事一覧/API & SDK
⬡ API & SDK/2026-04-26中級

Claude API のプロンプトキャッシュで月額コストを半分にした実装メモ

長文のシステムプロンプトを毎リクエスト送る本番ワークロードでは、プロンプトキャッシュの設計次第で月額コストが大きく変わります。ヒット率の計測から始める手順、TTLの選択、トークン下限と4ブロック上限、toolsを1つ足してヒット率が0になった失敗談に加え、ヒット率から入力コストを逆算する計算と損益分岐(5分TTLで約22%、1時間TTLで約53%)まで、半年の運用実例でまとめました。

claude-api83prompt-caching15cost-optimization28production87anthropic12

Anthropic API のプロンプトキャッシュを使い始めて半年ほど経ちました。最初は半信半疑だったのですが、長文のシステムプロンプトを毎リクエスト送る本番ワークロードでは、請求額が体感で半分以下になります。一方で、設計を間違えるとほぼ何の効果もない、という落差が大きい機能でもあります。

この記事は、私が個人運営している複数のアプリケーションで実際にプロンプトキャッシュを導入して、月額コストの推移を追いながら設計を直していった記録です。公式ドキュメントには載っていない「現場で気づいた挙動」も含めて整理します。

なぜ私はプロンプトキャッシュを真剣に検討したのか

きっかけは恥ずかしい話ですが、API の請求額が想定の3倍に膨れていたことでした。チャット系のサービスでは、システムプロンプト(前提情報・ペルソナ・出力フォーマット指定)が長くなりがちで、私のケースでは6,000〜10,000トークンほど。これを毎ターン送り直していました。

20往復の会話で、システムプロンプトだけで12万トークン以上を消費している計算です。リクエスト本体のコンテキスト処理に埋もれて見えていませんでしたが、内訳を見て愕然としました。

プロンプトキャッシュは、この「同じ前提情報を毎回送る」コストを大きく下げてくれます。具体的には、キャッシュヒット時の入力トークン単価が標準の10分の1になります(書き込み時は1.25倍ですが、これは初回のみ)。

ただし、効果が出るかどうかは「プロンプトのどこに静的な部分があるか」「アクセスパターンがどうか」に強く依存します。

プロンプトキャッシュが効くプロンプト構造とは

公式の cache_control パラメータを使うと、プロンプトの特定の位置までを「キャッシュ対象」として指定できます。Python SDK だと次のようになります。

from anthropic import Anthropic
 
client = Anthropic()
 
response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    system=[
        {
            "type": "text",
            "text": LONG_STATIC_SYSTEM_PROMPT,  # 数千トークン
            "cache_control": {"type": "ephemeral"}
        }
    ],
    messages=[
        {"role": "user", "content": user_input}
    ]
)

cache_control を付けた要素までがキャッシュされます。重要なのは、キャッシュは「プロンプトの先頭からその位置まで」が完全一致した場合にのみヒットすることです。つまり、キャッシュ対象の前にユーザーごとに変わる情報が混ざっていると、ヒットしません。

私が最初にハマったのはこれでした。ユーザー名やセッション情報をシステムプロンプトの冒頭に書いていたため、キャッシュは存在するのに毎回ミスしていたのです。

正しい配置は、静的な情報を先頭に、動的な情報を後ろに置くことです。

system=[
    {
        "type": "text",
        "text": STATIC_PROMPT,        # 動かない部分(ペルソナ・ルール・例示)
        "cache_control": {"type": "ephemeral"}
    },
    {
        "type": "text",
        "text": f"User context: {user_id}, time: {now}"  # 動的部分
    }
]

キャッシュヒット率を測ることから始める

導入してすぐにやるべきは、ヒット率の計測です。レスポンスの usage には次のようなフィールドが含まれます。

print(response.usage)
# Usage(
#   input_tokens=120,
#   cache_creation_input_tokens=0,
#   cache_read_input_tokens=8500,
#   output_tokens=300
# )

cache_read_input_tokens がキャッシュヒットしたトークン数、cache_creation_input_tokens が新規にキャッシュ書き込みされたトークン数です。input_tokens は通常通り課金される入力トークン。

私は最低限、次のメトリクスを記録しています。

def log_cache_metrics(response, request_id):
    usage = response.usage
    total_input = (
        usage.input_tokens
        + usage.cache_creation_input_tokens
        + usage.cache_read_input_tokens
    )
    hit_rate = usage.cache_read_input_tokens / total_input if total_input else 0
 
    logger.info({
        "request_id": request_id,
        "cache_hit_rate": hit_rate,
        "cache_read": usage.cache_read_input_tokens,
        "cache_write": usage.cache_creation_input_tokens,
        "input_uncached": usage.input_tokens,
        "output": usage.output_tokens,
    })

ヒット率が80%を超えていれば設計は概ね正しいです。50%を切る場合は、キャッシュ境界の前に動的な情報が混ざっている可能性が高いので、プロンプトを再点検します。

TTL(Time To Live)の選び方

ephemeral キャッシュのデフォルト TTL は5分です。最後にアクセスされてから5分経つと自動的に削除されます。チャットのような連続的な対話では十分ですが、たとえば「平日の昼休みだけ集中アクセスがある社内ツール」では、休憩時間の度にキャッシュが消えて再書き込みになります。

長めの TTL(1時間)を使う場合は、cache_control に ttl を指定します。

"cache_control": {"type": "ephemeral", "ttl": "1h"}

ただし、長い TTL は書き込みコストの単価が上がります(1時間 TTL は標準の2倍)。ヒット率が高ければ得ですが、低ければ損です。

私の経験則は次の通りです。ユーザーあたり1日10回以上アクセスがある定常ワークロードなら1時間 TTL が有利。深夜だけ動くバッチ処理など、再アクセスが見込めない場合は5分 TTL のままで十分です。サービスの利用パターンをログから読んでから決めるのが安全です。

トークン下限を意識する

プロンプトキャッシュには「最小キャッシュ可能トークン数」があります。Claude Sonnet 系では1,024トークン、Haiku 系では2,048トークンです。これより短いプロンプトには cache_control を付けても効きません。

意外と見落としがちなのが、システムプロンプトを「動的に組み立てる」関数です。たとえばユーザー権限によってルールを増減させていると、ある日は2,000トークンでキャッシュが効き、別の日は900トークンで効かない、ということが起きます。

私は本番サービスでは、システムプロンプトを「常に1,024トークン以上になる」ようにテンプレート化しました。例示セクションを充実させて、動的部分(ユーザー固有情報)はキャッシュ境界の外に置く設計です。これで安定してヒット率を稼げます。

4ブロック上限という制約

cache_control を付けられるのは1リクエストにつき最大4ブロックまでです。長大な RAG 文書などをキャッシュしたい場合、無計画にブロックを切ると上限に引っかかります。

おすすめは、論理的に意味のある単位で4ブロック以内に分けることです。

system=[
    {"type": "text", "text": COMPANY_RULES, "cache_control": {"type": "ephemeral"}},
    {"type": "text", "text": OUTPUT_FORMAT_SPEC, "cache_control": {"type": "ephemeral"}},
    {"type": "text", "text": EXAMPLES, "cache_control": {"type": "ephemeral"}},
    {"type": "text", "text": USER_RAG_CONTEXT, "cache_control": {"type": "ephemeral"}},
]

それぞれのブロックは独立した「キャッシュ階層」を作るので、USER_RAG_CONTEXT だけが変わってもその前の3ブロックはヒットします。これは長期記憶を持つボットを作るときに強力です。

tools を1つ足した翌日、ヒット率が0になった

半年のあいだで一番痛かったのはこれでした。サポートボットに小さなツールを1つ追加しただけの日です。プロンプトには一文字も触れていません。それなのに翌朝のヒット率は0%でした。

キャッシュの前方一致は tools → system → messages の順で判定されます。ツール定義はプロンプトの最も手前に置かれるため、そこが変わると後ろにある system のキャッシュまで巻き添えで無効になります。

client.messages.create(
    model="claude-sonnet-4-6",
    tools=[search_tool, calc_tool],   # ← ここが1つ増えると
    system=[
        {"type": "text", "text": STATIC_PROMPT,
         "cache_control": {"type": "ephemeral"}},   # ← ここも作り直しになる
    ],
    messages=[...],
)

対策は素朴です。ツールの追加・変更は週1回のリリースにまとめ、その日はヒット率が落ちるものとして扱います。監視のアラート閾値も、リリース日だけ緩めてあります。

逆に、ツール定義が安定しているサービスなら tools の末尾に cache_control を置いて、ツール定義ごとキャッシュしてしまうほうが得です。JSON Schema の長いツールを10個も抱えていると、それだけで数千トークンになりますので。

会話履歴そのものをキャッシュする

システムプロンプトだけをキャッシュしていた頃、私はまだ半分しか節約できていませんでした。20往復する会話では、ターンが進むほど messages 側が膨らみます。10往復目には履歴だけで数千トークン。ここが毎回まるごと再計算されていました。

cache_control は messages の要素にも置けます。直前のアシスタント応答に印を付けると、そこまでの履歴がキャッシュされます。

def with_history_cache(messages: list[dict]) -> list[dict]:
    msgs = [dict(m) for m in messages]
    if not msgs:
        return msgs
    last = msgs[-1]
    content = last["content"]
    if isinstance(content, str):
        content = [{"type": "text", "text": content}]
    content = [dict(c) for c in content]
    content[-1]["cache_control"] = {"type": "ephemeral"}
    last["content"] = content
    return msgs

次のターンでは、その印より手前(システムプロンプトと過去の履歴)が丸ごとヒットします。新規に課金されるのは直近のやりとりの差分だけ。

気をつけるのは、4ブロックの上限を履歴で食い潰さないことです。私は「システムプロンプトに1つ、FAQ に1つ、履歴の末尾に1つ」の3枠に固定し、残り1枠は予備として空けています。履歴の印は毎ターン末尾へ移すだけで、増やしません。

この設計にしてから、長い会話でのターンあたり入力コストが目に見えて平らになりました。それまでは会話が長引くほど1ターンの単価が上がっていたのですが、いまはほぼ横ばいです。

RAG ワークロードでの実装パターン

実際に私が個人開発のサポートボットで使っている設計を共有します。FAQ データを Claude にロードして応答させるシンプルな RAG です。

def build_messages(user_query: str, retrieved_docs: list[str]) -> dict:
    return {
        "system": [
            {
                "type": "text",
                "text": SYSTEM_PROMPT_BASE,  # ペルソナと出力ルール
                "cache_control": {"type": "ephemeral"}
            },
            {
                "type": "text",
                "text": FAQ_KNOWLEDGE_BASE,  # 約50KB の固定 FAQ
                "cache_control": {"type": "ephemeral"}
            },
            {
                "type": "text",
                "text": "\n".join(retrieved_docs)  # クエリごとに変わる
            }
        ],
        "messages": [
            {"role": "user", "content": user_query}
        ]
    }

このパターンで、FAQ 部分のキャッシュヒット率は95%以上を維持できています。検索結果(retrieved_docs)はキャッシュ外なので毎回新規ですが、これは仕方ありません。

トータルで見ると、月額コストは導入前の38%まで下がりました。当初の半分どころか3分の1近くです。

ヒットしないときに疑うべき5つのポイント

これは現場で何度も書いた「自分用チェックリスト」です。

第一に、cache_control を付けた要素より前に、動的な内容が混ざっていないか。私は最初これでつまずきました。

第二に、最小トークン数を満たしているか。1,024トークン未満では Sonnet ではキャッシュされません。

第三に、TTL 切れではないか。5分以上アクセスが空くと自動的に削除されます。

第四に、モデルが違っていないか。claude-sonnet-4-6 と claude-sonnet-4-5 でキャッシュは別になります。バージョンアップ時はキャッシュが一旦リセットされます。

第五に、空白や改行の差分はないか。文字列が1バイトでも違うとヒットしません。プロンプトを動的生成しているなら、テンプレート関数の挙動を疑います。

月額コスト推移の実例

参考までに、私のサポートボット(月間リクエスト数 約12,000)でのコスト推移を共有します。

時期月額ヒット率そのとき変えたこと
導入前$187(約 ¥28,000)—システムプロンプト約8,000トークン × 平均20往復を毎回送信
1週間後$142(約 ¥21,000)約30%キャッシュ境界の置き方を間違えたまま
1ヶ月後$79(約 ¥12,000)92%動的部分を境界の後ろへ移動、TTL を1時間に
3ヶ月後$71(約 ¥10,500)95%RAG 文書と会話履歴もキャッシュ対象に追加

導入前との比較で約62%の削減です。目を引くのは1週間後の行で、キャッシュは効いているのにヒット率30%だと削減幅は24%止まり。同じ機能を入れていても、境界の置き方ひとつで結果が3倍変わります。

プロンプト長が伸びるほど恩恵も伸びますので、長文プロンプトを毎リクエスト送っているサービスほど、先に手を付ける価値があります。

損益分岐は思っているより手前にある

「書き込みは1.25倍かかる」と聞くと、何十回も読まれないと元が取れない気がします。私もそう思い込んで、アクセスの少ないエンドポイントには入れずにいました。紙の上で計算してみたら、まったく逆でした。

標準入力を1.0とすると、書き込みは1.25、読み出しは0.1。書き込みで余計に払うのは0.25です。一方、1回ヒットするたびに0.9を節約します。

TTL書き込み単価余分に払う分1ヒットの節約回収に必要なヒット
5分1.25×0.25×0.9×1回
1時間2.0×1.0×0.9×2回

5分 TTL なら1回ヒットするだけで書き込み分は回収できます。1時間 TTL でも2回です。

本当のリスクは「書き込みが高いこと」ではなく、「一度もヒットしないまま TTL が切れること」でした。書き込んで0回なら素直に0.25倍(1時間なら1.0倍)の損です。判断の軸は「何回ヒットするか」ではなく「1回でもヒットするか」。そう置き換えてから、迷っていたエンドポイントには全部キャッシュを入れました。

私自身、この計算を先に紙の上でやっていれば、判断を半年ぶんも遅らせずに済んだはずです。単価表を眺めて怯むより、割り算を一度やってしまうほうが早いという当たり前のことを、遠回りして学びました。

ヒット率から請求額を逆算してみる

表の「1週間後」の行が気になって、ヒット率と入力コストの関係を割り算で書き出してみました。前提は単純です。キャッシュ対象部分について、ヒットなら読み出し(0.1倍)、ミスなら書き込み(5分 TTL で1.25倍、1時間 TTL で2倍)が掛かるものとします。出力トークンと、キャッシュ対象外の動的部分は含めていません。

def input_cost_ratio(hit_rate: float, write_mult: float) -> float:
    """キャッシュ対象部分のコストを、キャッシュなし=1.0 とした倍率で返す。
    ヒットなら読み出し 0.1、ミスなら書き込み write_mult が掛かるモデル。"""
    return hit_rate * 0.1 + (1 - hit_rate) * write_mult
 
 
def break_even(write_mult: float) -> float:
    """倍率が 1.0 を下回り始めるヒット率。"""
    return (write_mult - 1.0) / (write_mult - 0.1)
 
 
for p in (0.3, 0.5, 0.8, 0.92, 0.95):
    r5 = input_cost_ratio(p, 1.25)
    r1 = input_cost_ratio(p, 2.0)
    print(f"{p:>4.0%}  5分: {r5:.3f}  1時間: {r1:.3f}")

手元で実行した出力を、キャッシュなしを1.0とした倍率に直したのが次の表です。

ヒット率5分 TTL の倍率1時間 TTL の倍率
30%0.905(9.5%減)1.430(43%増)
50%0.675(32.5%減)1.050(5%増)
80%0.330(67%減)0.480(52%減)
92%0.192(81%減)0.252(75%減)
95%0.158(84%減)0.195(80%減)

損益分岐のヒット率は、5分 TTL で約22%、1時間 TTL で約53%でした。先ほどの「1時間 TTL は2回ヒットで回収」という表と、同じことを別の角度から見ています。

この表で気づいたことが2つあります。

ひとつは、ヒット率30%の行です。キャッシュ対象部分の入力コストは1割も減らず、請求書の削減幅が24%にとどまったのは、計算上はむしろ健闘していました。「機能は入れたのに安くならない」と感じる段階では、設計が悪いというより、ヒット率がまだ分岐点の少し先にいるだけです。

もうひとつは、1時間 TTL を選ぶ条件です。ヒット率が5割前後なら、1時間 TTL は5分 TTL より高くつきます。私のサポートボットは、TTL を伸ばしたときに92%まで上がりましたが、あれは TTL そのものの効果ではなく、境界を動かした効果が大半だったのかもしれません。TTL は「ヒット率をどれだけ押し上げるか」と引き換えに書き込み単価を払う選択なので、変更前後のヒット率を並べて見ないと損得は判断できません。

数字を見て TTL を変えるなら、先に5分のままヒット率を測ります。そのうえで、分岐点の53%をはっきり超える見込みがあるときだけ1時間に切り替えます。いまはその順序で運用しています。

次にやること

この記事を読んで「うちでも効きそうだ」と思った方は、まず1日分のリクエストログを取って、システムプロンプトの長さとリクエスト頻度を見てみてください。最小トークン数(1,024)を超えていて、同じ前提情報を繰り返し送っているなら、ほぼ確実に恩恵があります。

実装は「system を配列にして cache_control を付ける」だけなので、半日もあれば組めます。難しいのは設計の方で、特に「どこを境界にするか」と「どう計測するか」が肝心です。この記事のチェックリストを手元に置いて、ヒット率を見ながら境界を動かしてみてください。

最初の数日でヒット率を80%まで持っていけたら、コスト削減はほぼ確実です。

シェア

お読みいただきありがとうございます

Claude Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

⬡ API & SDK2026-09-07
キャッシュ読み取りが 75% 安くなっても、請求の下がり方は構成しだいで 10 倍違います
Fable 5.1 と Mythos 5.1 でキャッシュ読み取りの倍率が 0.1 から 0.025 へ変わりました。自分のトークン内訳から削減幅を先に見積もる手順と、倍率を定数で持つコードの直し方をまとめます。
⬡ API & SDK2026-04-29
Claude API のセマンティックキャッシュを本番投入する — 類似プロンプト判定としきい値設計、汚染防止
類似プロンプトへの応答を再利用するセマンティックキャッシュをClaude APIの本番に組み込む設計を一気通貫で解説します。embeddingと類似度しきい値の決め方、マルチテナント分離、キャッシュ汚染を防ぐガード層、フォールバックと劣化モード、プロンプトキャッシュとの重ね方、効果を測る観測性の指標まで踏んだ地雷とともに紹介します。
⬡ API & SDK2026-04-12
Claude API インテリジェントモデルルーティング — Sonnet 4.6 / Haiku 4.5 自動選択でコストと品質を両立する設計パターン
リクエスト内容に応じてClaude Sonnet 4.6とHaiku 4.5を自動切り替えするインテリジェントルーティングの実装。分類器の設計からサーキットブレーカー、コスト監視まで本番運用パターンを網羅