長い 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 単価比 補足 通常 input 1.0× 非キャッシュ処理 Cache write (5 分 TTL) 1.25× 書き込み時の一度だけ Cache write (1 時間 TTL) 2.0× extended キャッシュ Cache read 0.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 スタック構成 を書く予定です。
参考
$ tree ./repo/
記事で登場したコードをひとつの最小リポジトリとしてまとめました。ファイルツリーから切り替えて、全体の構造をそのまま確認できます。
┌─ claude-api-prompt-caching ────────── files 08 ────────────────────── ● ● ● ─┐ $ cat ./package.json json
# このファイルを .env にコピーして値を入れる
# (`.env` は gitignore 推奨)
# Anthropic API Key — https://console.anthropic.com/settings/keys
ANTHROPIC_API_KEY= # claude-api-prompt-caching
Claude API の prompt caching を正しく使うための最小テンプレート。
[ 記事本文 ]( https://r43lab.com/blog/claude-api-prompt-caching ) と対になる実動コード集です。
## Quick start
```bash
bun install
cp .env.example .env # ANTHROPIC_API_KEY を入れる
# 1. 基本: system プロンプトをキャッシュ
bun run src/basic.ts
# 2. multi-turn で prefix を育てる
bun run src/multi-turn.ts
# 3. 1 時間 TTL (extended cache)
bun run src/extended.ts
# 4. hit rate の定量測定
bun run src/usage-report.ts
```
## ファイル構成
- `src/basic.ts` — system ブロックに cache_control を 1 箇所だけ置く最小例
- `src/multi-turn.ts` — 会話のターンを重ねながらキャッシュを維持する
- `src/extended.ts` — `ttl: "1h"` を使うバースト用途向け
- `src/usage-report.ts` — `cache_read_input_tokens` から hit rate と節約額を算出
- `.env.example` — ANTHROPIC_API_KEY のテンプレ
## 2026-04 時点の最小キャッシュサイズ(重要)
モデルごとにキャッシュ成立する最小 prefix トークン数が違います。これを下回ると静かに miss します。
| モデル | 最小 prefix tokens |
|---|---:|
| Opus 4.7 / Opus 4.6 / Haiku 4.5 | 4,096 |
| Sonnet 4.6 | 2,048 |
`src/basic.ts` のサンプル system prompt は 4,096 以上に膨らむよう水増ししてあります。実運用で短い system prompt に cache_control を付けても効かないのはこのため。
## 依存バージョンについて
`package.json` は **記事執筆時点 (2026-04) のスナップショット** です。Anthropic SDK は継続的に更新されるので、clone 後は最新化推奨。
```bash
bun outdated
bun update
```
## Stack
Bun · TypeScript · `@anthropic-ai/sdk` v0.90 {
"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"
}
} /**
* basic.ts — system プロンプトをキャッシュする最小例
*
* Opus 4.7 / Haiku 4.5 は最小 4,096 tokens、Sonnet 4.6 は最小 2,048 tokens を
* 超えた時点でキャッシュが成立する。以下の SHARED_SYSTEM は水増しして
* 十分長くしてある(英文+コード例 ~6,000 tokens 相当)。
*
* 実行: bun run src/basic.ts (2 回続けて走らせて usage の変化を観察する)
*/
import Anthropic from "@anthropic-ai/sdk" ;
const client = new Anthropic ();
// 実運用では RAG で引いた文脈や長い few-shot を置く想定。
// ここではキャッシュ成立条件を満たすためにダミーの長文を敷いている。
const SHARED_SYSTEM = `
You are a senior TypeScript reviewer that writes feedback in Japanese.
## 観点
1. 型安全性が実質的に壊れていないか (any / unknown / as で逃げていないか)
2. null / undefined の扱いが実行時エラーにならないか
3. 非同期処理の順序と失敗経路が明示されているか
4. 責務分割が実装都合ではなく読み手にとって妥当か
5. テスト可能性 (依存を外から注入できる形か)
## 出力フォーマット
- まず「結論」を 1 行
- 次に「良い点」を 3 点まで
- 次に「気になる点」を 5 点まで、各点に一行の修正提案を添える
- 最後に「総評」を 2 行以内
... ${"追加の詳細な指示は省略。実運用では 4,000+ tokens の静的コンテンツを配置する。" . repeat ( 80 ) }
` . trim ();
async function run () {
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:
"次の TypeScript のコードを 300 字以内でレビューしてください: `const foo: any = getFoo(); foo.bar.baz()`" ,
},
],
});
console. log ( "--- response ---" );
for ( const block of resp.content) {
if (block.type === "text" ) console. log (block.text);
}
console. log ( " \n --- usage ---" );
console. log ({
input_tokens: resp.usage.input_tokens,
cache_creation_input_tokens: resp.usage.cache_creation_input_tokens,
cache_read_input_tokens: resp.usage.cache_read_input_tokens,
output_tokens: resp.usage.output_tokens,
});
console. log (
" \n 初回は cache_creation_input_tokens に大きな値、2 回目以降は cache_read_input_tokens に入る。" ,
);
}
run (). catch (( err ) => {
console. error (err);
process. exit ( 1 );
}); /**
* extended.ts — 1 時間 TTL の extended caching
*
* `ttl: "1h"` を指定すると書き込み単価は base の 2.0 倍になる。
* 5 分 TTL (1.25x) より高い代わりに、30 分-1 時間隔のバーストを確実に拾える。
*
* 経済性:
* 5 分 TTL — 2 回使えば元が取れる (1.25 + 0.1 = 1.35 < 2.0)
* 1 時間 TTL — 3 回以上使う見込みが要る (2.0 + 0.2 = 2.2 < 3.0)
*
* 向く: cron 監視系、30 分ごとの定型レポート、ユーザー当たり数回だが
* 長い対話間隔が開くバッチ系など。
*/
import Anthropic from "@anthropic-ai/sdk" ;
const client = new Anthropic ();
const SHARED_KB = `
組織内 FAQ の参照コンテキスト。以下は全てキャッシュ対象。
... ${"社内ドキュメントの抜粋や運用手順を想定した文脈の水増し。" . repeat ( 200 ) }
` . trim ();
async function askWithExtendedCache ( q : string ) {
return client.messages. create ({
model: "claude-opus-4-7" ,
max_tokens: 800 ,
system: [
{
type: "text" ,
text: SHARED_KB ,
cache_control: { type: "ephemeral" , ttl: "1h" }, // ← extended
},
],
messages: [{ role: "user" , content: q }],
});
}
async function main () {
const r = await askWithExtendedCache (
"FAQ の中から、オンボーディング関連の項目を 3 つ抽出して要点を述べて" ,
);
console. log (
r.content
. filter (( b ) => b.type === "text" )
. map (( b ) => (b as { text : string }).text)
. join ( "" ),
);
console. log ( " \n usage:" , r.usage);
}
main (). catch (( err ) => {
console. error (err);
process. exit ( 1 );
}); /**
* multi-turn.ts — 会話のターンを跨いでキャッシュを育てる
*
* 最後のユーザー発話の末尾に cache_control を打つと、次のターンで
* 「前ターンまで丸ごと」が cache_read_input_tokens に計上される。
*
* 3 ターン走らせて、各ターンでどこまで再利用されるかを観察する。
*/
import Anthropic from "@anthropic-ai/sdk" ;
const client = new Anthropic ();
const SYSTEM = `あなたは TypeScript の熟練コーチで、出力は常に日本語。
生徒の書いたコードを元に、短い対話を通じて理解を深めさせる。
1 回の応答は 3-4 文以内。${"static 文脈の水増し。" . repeat ( 300 ) }` ;
type Turn = { role : "user" | "assistant" ; text : string };
async function ask ( history : Turn [], nextUser : string ) {
// 最後のユーザー発話にだけ cache_control を打つ。
// ここまでが次ターンのキャッシュ読み出しポイントになる。
const messages : Anthropic . MessageParam [] = [
... history. map (( h ) => ({
role: h.role,
content: h.text,
})),
{
role: "user" as const ,
content: [
{
type: "text" as const ,
text: nextUser,
cache_control: { type: "ephemeral" as const },
},
],
},
];
const resp = await client.messages. create ({
model: "claude-sonnet-4-6" ,
max_tokens: 512 ,
system: SYSTEM ,
messages,
});
const text = resp.content
. filter (( b ) => b.type === "text" )
. map (( b ) => (b as { text : string }).text)
. join ( " \n " );
console. log ( ` \n > user: ${ nextUser }` );
console. log ( `assistant: ${ text }` );
console. log ( "usage:" , {
in: resp.usage.input_tokens,
create: resp.usage.cache_creation_input_tokens,
read: resp.usage.cache_read_input_tokens,
});
return { text, resp };
}
async function main () {
const history : Turn [] = [];
// ターン 1: キャッシュ作成
let r = await ask (history, "let vs const の違いを初心者向けに教えて" );
history. push (
{ role: "user" , text: "let vs const の違いを初心者向けに教えて" },
{ role: "assistant" , text: r.text },
);
// ターン 2: 前ターンまでキャッシュ読み出し + 新しい breakpoint 作成
r = await ask (history, "再代入ってどういう時に困りそう?" );
history. push (
{ role: "user" , text: "再代入ってどういう時に困りそう?" },
{ role: "assistant" , text: r.text },
);
// ターン 3: ターン 2 までの長い prefix を丸ごと読み出し
r = await ask (history, "じゃあ const を使うべきじゃない場面を 1 つ挙げて" );
}
main (). catch (( err ) => {
console. error (err);
process. exit ( 1 );
}); /**
* usage-report.ts — キャッシュ hit rate と節約額の定量測定
*
* 任意の Anthropic.Message を入力にして、
* - hit rate (cache_read / 全 input)
* - 仮のコスト比 (base 1x を基準に、write 1.25x + read 0.1x のミックスを試算)
* を出す。複数リクエストをまとめてレポートするためのユーティリティ。
*/
import Anthropic from "@anthropic-ai/sdk" ;
type Usage = Anthropic . Usage ;
/**
* 1 件分のキャッシュ効率を計算
*/
export function cacheReport ( usage : Usage ) {
const inp = usage.input_tokens ?? 0 ;
const w = usage.cache_creation_input_tokens ?? 0 ;
const r = usage.cache_read_input_tokens ?? 0 ;
const total = inp + w + r;
const hitRate = total > 0 ? r / total : 0 ;
// ベース入力を 1 として、このリクエストの実コスト倍率
const multiplier = total > 0 ? (inp * 1.0 + w * 1.25 + r * 0.1 ) / total : 1 ;
return {
total,
breakdown: { input_tokens: inp, cache_create: w, cache_read: r },
hitRate,
multiplier, // 1.0 が非キャッシュ、0.1 が完全ヒット
};
}
/**
* 複数リクエストを束ねてレポート
*/
export function aggregate ( usages : Usage []) {
const sum = usages. reduce (
( acc , u ) => ({
input: acc.input + (u.input_tokens ?? 0 ),
create: acc.create + (u.cache_creation_input_tokens ?? 0 ),
read: acc.read + (u.cache_read_input_tokens ?? 0 ),
}),
{ input: 0 , create: 0 , read: 0 },
);
const total = sum.input + sum.create + sum.read;
const effectiveCost = sum.input * 1.0 + sum.create * 1.25 + sum.read * 0.1 ;
const hypotheticalBase = total * 1.0 ;
return {
requests: usages. length ,
totalInputTokens: total,
breakdown: sum,
hitRate: total > 0 ? sum.read / total : 0 ,
costVsNoCache: total > 0 ? effectiveCost / hypotheticalBase : 1 ,
saved: hypotheticalBase - effectiveCost,
};
}
// 簡易デモ: ダミーの usage 3 件で動作確認
if ( import . meta .main) {
const fakeUsages : Usage [] = [
{ input_tokens: 120 , cache_creation_input_tokens: 4200 , cache_read_input_tokens: 0 , output_tokens: 300 } as Usage ,
{ input_tokens: 80 , cache_creation_input_tokens: 0 , cache_read_input_tokens: 4200 , output_tokens: 280 } as Usage ,
{ input_tokens: 95 , cache_creation_input_tokens: 0 , cache_read_input_tokens: 4200 , output_tokens: 310 } as Usage ,
];
console. log ( "--- per request ---" );
fakeUsages. forEach (( u , i ) => {
const r = cacheReport (u);
console. log (
`#${ i + 1 } hit=${ ( r . hitRate * 100 ). toFixed ( 1 ) }% mult=${ r . multiplier . toFixed ( 3 ) }x` ,
);
});
console. log ( " \n --- aggregate ---" );
console. log ( aggregate (fakeUsages));
} {
"compilerOptions" : {
"target" : "ES2022" ,
"module" : "ESNext" ,
"moduleResolution" : "Bundler" ,
"lib" : [ "ES2022" ],
"types" : [ "bun" ],
"strict" : true ,
"noEmit" : true ,
"skipLibCheck" : true ,
"resolveJsonModule" : true ,
"isolatedModules" : true ,
"verbatimModuleSyntax" : true ,
"allowImportingTsExtensions" : true
},
"include" : [ "src/**/*" ]
} slug claude-api-prompt-caching│ files 8 click a file in the tree to switch