Claude API のプロンプトキャッシュでコストを 1/10 にする

Anthropic の Claude API には ephemeral prompt caching が組み込まれており、正しく配置すればキャッシュ読み出しの単価は base input の 0.1 倍にまで下がります。5 分 / 1 時間 TTL、配置できる場所、キャッシュが効かない silent invalidators、multi-turn 会話の維持パターンを、2026-04 時点の仕様で整理します。

長い system prompt や few-shot、ツール定義を抱えた Claude API リクエストは、素直に投げると入力トークンで膨らみます。これを劇的に抑えるのが prompt caching。正しく配置すれば、キャッシュから読まれた入力トークンの単価は base input の 0.1 倍まで下がります。本記事では、2026-04 時点の最新仕様と実装パターンを、一次ソースに照らして整理します。

到達点

  • cache_control: { type: "ephemeral" } を system / tools / messages の適切な位置に置く
  • 2 回目以降の同一 prefix リクエストで キャッシュ読み出し単価 = base の 0.1 倍
  • multi-turn 会話でも prefix を育てながらキャッシュを連鎖させる
  • usage.cache_read_input_tokens で効果を定量確認

検証モデル: Claude Opus 4.7 / Sonnet 4.6 / Haiku 4.5、SDK: @anthropic-ai/sdk v0.90 系。

キャッシュの価格構造

同じ入力トークンでも、書き込み・読み出し・通常処理で単価が変わります。

項目base 単価比補足
通常 input1.0×非キャッシュ処理
Cache write (5 分 TTL)1.25×書き込み時の一度だけ
Cache write (1 時間 TTL)2.0×extended キャッシュ
Cache read0.1×ヒットした分は全てこの単価

5 分 TTL は 2 回使えば損益分岐(1.25 + 0.1 = 1.35 < 2.0)。1 時間 TTL は 3 回以上使う見込みがあるときに有効(2.0 + 0.2 = 2.2 < 3.0)。

仕組みの基本

キャッシュキーは prefix の実バイト列から派生します。tools → system → messages の render 順で、位置 N のバイトを 1 つでも変えると、N 以降のすべての breakpoint が無効化されます。

  • cache_control: { type: "ephemeral" }system ブロック / tool 定義 / messages の text / image / document / tool_use / tool_result に配置可能
  • breakpoint は 1 リクエストあたり最大 4 個
  • 最小キャッシュサイズはモデル依存(これを下回ると無言でキャッシュされない):
    • Opus 4.7 / Opus 4.6 / Haiku 4.5: 4,096 tokens
    • Sonnet 4.6: 2,048 tokens

「Haiku なら小さくても効く」という旧来の直感は現行 Haiku 4.5 では逆で、Opus と同じ 4,096 tokens 必要です。

実装 1: システムプロンプトの基本形

最も頻出するユースケース。重たい共有 system prompt の末尾にマーカーを置きます。

import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();
const SHARED_SYSTEM = "…大きな system prompt(数 KB)…";

const resp = await client.messages.create({
  model: "claude-opus-4-7",
  max_tokens: 1024,
  system: [
    {
      type: "text",
      text: SHARED_SYSTEM,
      cache_control: { type: "ephemeral" }, // 5 分 TTL
    },
  ],
  messages: [{ role: "user", content: "要点を 3 行で" }],
});

console.log(resp.usage);
// 初回:  { input_tokens: 12, cache_creation_input_tokens: 3800, cache_read_input_tokens: 0 }
// 2 回目: { input_tokens: 12, cache_creation_input_tokens: 0,    cache_read_input_tokens: 3800 }

input_tokens は、厳密には「最後の cache breakpoint より後のトークン」を指します。同じ system で 2 回目以降はこれが極小になり、直前まで書き込んだ分が cache_read_input_tokens に積まれるのが目印です。

実装 2: multi-turn 会話でキャッシュを育てる

会話ごとに全履歴を再送する Claude API の性質上、最後のユーザー発話の末尾cache_control を付けると、次のターンはその地点までが丸ごとキャッシュ読み出しになります。

const messages: Anthropic.MessageParam[] = [
  { role: "user", content: "…最初の質問…" },
  { role: "assistant", content: "…前ターンの回答…" },
  {
    role: "user",
    content: [
      {
        type: "text",
        text: "ここまでを踏まえて追加で…",
        cache_control: { type: "ephemeral" }, // 次のターンまで保持
      },
    ],
  },
];

await client.messages.create({
  model: "claude-sonnet-4-6",
  max_tokens: 1024,
  system: SHARED_SYSTEM,
  messages,
});

ターンを重ねても、前のターンまでの breakpoint はそのまま読み出しポイントとして有効です(breakpoint は 20 ブロック以内を後ろ向きに探索して直近のキャッシュを拾うので、長い tool loop では 15 ブロックごとに 1 つ breakpoint を打つのが安全)。

実装 3: 1 時間 TTL(バースト用途)

5 分より長い間隔でも確実にキャッシュを生かしたい場合は TTL を明示します。

system: [
  {
    type: "text",
    text: SHARED_SYSTEM,
    cache_control: { type: "ephemeral", ttl: "1h" }, // 書き込み単価 2.0×
  },
]

30 分〜1 時間おきにリクエストが飛ぶ監視系・バッチ送信など、5 分 TTL だと毎回 miss する運用で有効です。書き込みコストが 2.0× まで上がるので、3 回以上ヒットする確信がないなら 5 分のままで十分。

キャッシュが効かない典型パターン

cache_read_input_tokens がゼロのまま増えないときは、ほぼこのどれかです。

  • Date.now() / new Date().toISOString() を system prompt に埋め込んでいる — 1 バイトでも prefix が変われば全部無効化
  • tools 配列を毎回 Array.from(set) 等で組み立てている — 順序が非決定的になり、tools の position 0 で毎回 miss。tool 追加時は末尾に足す・順序を固定する
  • request ID や UUID を system に混ぜている — 同上
  • ユーザーごとに system プロンプトを切り替えている — キャッシュはユーザー間で共有されない
  • prefix の長さが最小値未満 — Opus 4.7 で 3,000 tokens 程度だと静かに cache miss。マーカーは無視される(エラーは出ない)
  • モデルを途中で切り替える — キャッシュはモデル単位で作られるため(model パラメータも exact match 対象)、別モデルを呼び直すと同じ prefix でも再度 write が発生する。サブエージェントを Haiku に逃がす等の並行利用ならコアの会話キャッシュは無傷で残る

効果の確認方法

運用で重要なのは以下 3 フィールドの合計を見ること:

const u = resp.usage;
const total = (u.input_tokens ?? 0)
            + (u.cache_creation_input_tokens ?? 0)
            + (u.cache_read_input_tokens ?? 0);
const hitRate = (u.cache_read_input_tokens ?? 0) / total;
console.log(`hit rate: ${(hitRate * 100).toFixed(1)}%`);

長時間走らせたのに input_tokens が 4K しかない、が正しく動いているサイン。全体のトークン数は合計で見ないと過小評価します。

まとめ

prompt caching は 配置より先に「prefix を動かさない」設計。frozen system / deterministic tools / volatile data を messages の末尾に寄せる、これさえ守れば cache_control は 1〜2 箇所で足ります。次回は 個人開発で月 10 万円を目指す Cloudflare スタック構成 を書く予定です。

参考

記事で登場したコードをひとつの最小リポジトリとしてまとめました。ファイルツリーから切り替えて、全体の構造をそのまま確認できます。

$cat./package.jsonjson
{
  "name": "claude-api-prompt-caching",
  "private": true,
  "type": "module",
  "scripts": {
    "basic": "bun src/basic.ts",
    "multi": "bun src/multi-turn.ts",
    "extended": "bun src/extended.ts",
    "usage": "bun src/usage-report.ts"
  },
  "dependencies": {
    "@anthropic-ai/sdk": "^0.90.0"
  },
  "devDependencies": {
    "@types/bun": "^1.3.0",
    "typescript": "^6.0.3"
  }
}
slugclaude-api-prompt-cachingfiles8click a file in the tree to switch