この記事ではClaude APIに特化して解説します。Claude API・料金全般は Claude 料金まとめ|Claude Code・API・Opus 4.7の価格を計算シミュレーター付きで比較 をご覧ください。
何が起きたか — 「自分の使い方が悪い」は誤解だった
Claude APIには、同じ指示を繰り返し送るときに料金を大幅に安くする「プロンプトキャッシュ」という節約機能がある。一度送った長い指示文をサーバーに記憶させ、2回目以降は料金を10分の1にする仕組みだ。
ところが、「キャッシュが効いているはずなのに、なぜか料金が高い」という報告がユーザーから相次いでいた。多くの開発者は「自分のコードが間違っているのでは」と疑っていた。
今回、Claude Codeのソースコード流出で見つかった内部ファイル promptCacheBreakDetection.ts に、答えが書いてあった。
“~90% of breaks are server-side routing/eviction or billed/inference disagreement”
(キャッシュが壊れる原因の約90%は、サーバー側のルーティング・削除・課金判定の不一致)
つまり、ユーザーがどんなに正しくコードを書いても、90%の確率でAnthropic側の問題でキャッシュが壊れる。 Anthropicは内部分析でこの事実を把握していたが、ユーザーには知らされていなかった。
Anthropicからの公式声明はまだ発表されていない。以下の分析は、流出コードと公開ドキュメントに基づく。
そもそもプロンプトキャッシュとは — 「同じ説明を毎回送らなくていい」機能
たとえばClaude APIで「1万文字のマニュアルを読んで質問に答えて」と100回繰り返すとする。毎回1万文字を送ると100回分の料金がかかる。
プロンプトキャッシュは、1回目に送ったマニュアルをサーバーに記憶させ、2回目以降は「さっきのマニュアル覚えてるよね?」で済ませる機能だ。2回目以降の料金は10分の1になる。
公式ドキュメントによれば、キャッシュは以下の条件で機能する。
cache_control: {type: "ephemeral"}をメッセージブロックに付与する- 最小キャッシュ可能トークン数はClaude 3.5 Sonnetで1024トークン
- キャッシュ有効期限は最長5分(Anthropicが内部的に管理)
import anthropic
client = anthropic.Anthropic()
# プロンプトキャッシュを有効にしたリクエスト例
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
system=[
{
"type": "text",
"text": "あなたは優秀なコードレビュアーです。以下のコーディング規約に従って...",
},
{
"type": "text",
"text": "<長大な規約ドキュメント>...", # 数千〜数万トークン
"cache_control": {"type": "ephemeral"}, # ここにキャッシュ指定
},
],
messages=[
{
"role": "user",
"content": "このPythonコードをレビューしてください: ..."
}
],
)
# キャッシュヒット率の確認
usage = response.usage
print(f"入力トークン: {usage.input_tokens}")
print(f"キャッシュ生成トークン: {usage.cache_creation_input_tokens}")
print(f"キャッシュ読み出しトークン: {usage.cache_read_input_tokens}")
なぜ「記憶してるはず」のキャッシュが壊れるのか
問題はここだ。「1回目に覚えさせた」はずなのに、2回目で「覚えてない」と言われ、フル料金を請求されるケースが頻発していた。
流出データから判明した「壊れる3つの原因」を、わかりやすく説明する。
1. 別の担当者に回された(サーバールーティング) Anthropicのサーバーは複数台ある。1回目はAサーバーに送られてキャッシュが保存されたが、2回目がBサーバーに回されると「Aに保存したキャッシュ」は見えない。結果、覚えていないことになる。
2. 混雑で記憶が消された(キャッシュ削除) キャッシュの有効期限は最長5分。だが5分以内でも、サーバーが混雑すると古いキャッシュから順番に消される。トラフィックが集中する時間帯に起きやすい。
3. 請求書と実態が食い違った(課金判定の不一致) 最も深刻な原因がこれ。課金システムは「キャッシュを使った」と判断して安い料金を記録したのに、実際の処理エンジンはキャッシュを使わなかった——あるいはその逆。請求額と実際の処理が一致していなかった可能性がある。
月額が3倍に跳ね上がる — キャッシュが壊れるとどうなるか
実際の料金でどれくらい影響があるのか。Claude APIのキャッシュ料金体系(2026年3月時点)を見る。
| トークン種別 | 料金(claude-opus-4-5の場合) |
|---|---|
| 通常入力トークン | $15 / 1Mトークン |
| キャッシュ生成トークン | $18.75 / 1Mトークン(通常の1.25倍) |
| キャッシュ読み出しトークン | $1.50 / 1Mトークン(通常の1/10) |
キャッシュが正常に機能した場合と機能しなかった場合では、コストに大きな差が生まれる。
前提条件の例:
- システムプロンプト: 10,000トークン
- ユーザーメッセージ: 500トークン
- 1日100回リクエスト
# キャッシュ効果のコスト計算スクリプト
def calculate_cache_cost(
system_tokens: int,
user_tokens: int,
requests_per_day: int,
hit_rate: float,
model: str = "claude-opus-4-5"
) -> dict:
"""
hit_rate: 0.0〜1.0(キャッシュヒット率)
"""
# claude-opus-4-5の料金($/ 1Mトークン)
pricing = {
"input": 15.00,
"cache_create": 18.75,
"cache_read": 1.50,
}
# 初回リクエスト(キャッシュ生成)のコスト
first_request_cost = (
(system_tokens * pricing["cache_create"]) +
(user_tokens * pricing["input"])
) / 1_000_000
# キャッシュヒット時のコスト
hit_cost = (
(system_tokens * pricing["cache_read"]) +
(user_tokens * pricing["input"])
) / 1_000_000
# キャッシュミス時のコスト(フル課金)
miss_cost = (
(system_tokens * pricing["input"]) +
(user_tokens * pricing["input"])
) / 1_000_000
# 1日あたりのコスト計算
daily_cost_with_cache = (
first_request_cost +
(requests_per_day - 1) * (
hit_rate * hit_cost +
(1 - hit_rate) * miss_cost
)
)
daily_cost_no_cache = requests_per_day * miss_cost
return {
"daily_cost_with_ideal_cache": first_request_cost + (requests_per_day - 1) * hit_cost,
"daily_cost_with_actual_cache": daily_cost_with_cache,
"daily_cost_no_cache": daily_cost_no_cache,
"monthly_savings": (daily_cost_no_cache - daily_cost_with_cache) * 30,
}
# 実行例
result = calculate_cache_cost(
system_tokens=10_000,
user_tokens=500,
requests_per_day=100,
hit_rate=0.5, # 実測のキャッシュヒット率50%
)
print(f"キャッシュなし: ${result['daily_cost_no_cache']:.2f}/日")
print(f"キャッシュあり(50%ヒット): ${result['daily_cost_with_actual_cache']:.2f}/日")
print(f"月間節約額: ${result['monthly_savings']:.2f}")
計算結果(概算):
| キャッシュヒット率 | 1日のコスト | 月間コスト | 月間節約額 |
|---|---|---|---|
| 0%(キャッシュなし) | $15.75 | $472.50 | — |
| 50%(実際の中間値) | $9.23 | $276.90 | $195.60 |
| 90%(理想的な状態) | $2.85 | $85.50 | $387.00 |
| 100%(完全キャッシュ) | $1.65 | $49.50 | $423.00 |
キャッシュヒット率が90%から50%に落ちるだけで、月間コストは約$85から$277に跳ね上がる。今回の流出が示すサーバー側起因の破壊が10%あるだけでも、コスト計画に直接的な影響が生まれる。
自分のキャッシュが壊れているか確認する方法
import anthropic
from dataclasses import dataclass, field
from typing import List
@dataclass
class CacheMetrics:
total_requests: int = 0
cache_hits: int = 0
cache_misses: int = 0
total_input_tokens: int = 0
total_cache_read_tokens: int = 0
total_cache_create_tokens: int = 0
def record(self, usage: anthropic.types.Usage):
self.total_requests += 1
self.total_input_tokens += usage.input_tokens
cache_read = getattr(usage, 'cache_read_input_tokens', 0) or 0
cache_create = getattr(usage, 'cache_creation_input_tokens', 0) or 0
self.total_cache_read_tokens += cache_read
self.total_cache_create_tokens += cache_create
if cache_read > 0:
self.cache_hits += 1
else:
self.cache_misses += 1
@property
def hit_rate(self) -> float:
if self.total_requests == 0:
return 0.0
return self.cache_hits / self.total_requests
def report(self):
print(f"総リクエスト数: {self.total_requests}")
print(f"キャッシュヒット: {self.cache_hits} ({self.hit_rate:.1%})")
print(f"キャッシュミス: {self.cache_misses}")
print(f"キャッシュ読み出しトークン計: {self.total_cache_read_tokens:,}")
print(f"キャッシュ生成トークン計: {self.total_cache_create_tokens:,}")
# 使用例
metrics = CacheMetrics()
client = anthropic.Anthropic()
for query in user_queries:
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
system=[
{"type": "text", "text": large_system_prompt,
"cache_control": {"type": "ephemeral"}}
],
messages=[{"role": "user", "content": query}]
)
metrics.record(response.usage)
metrics.report()
このスクリプトを本番環境に組み込むことで、実際のキャッシュヒット率を継続的にモニタリングできる。ヒット率が期待値を大幅に下回る場合は、今回流出した情報が示すサーバー側の問題が発生している可能性を疑うべきだ。
キャッシュが「当たる」か「外れる」かの分岐図
グループか?"} C -- No --> D["キャッシュミス
ルーティング起因"] C -- Yes --> E{"キャッシュが
有効期限内か?"} E -- No --> F["キャッシュミス
Eviction起因"] E -- Yes --> G{"課金システムと
推論エンジンが
一致するか?"} G -- No --> H["キャッシュミス
Billed/Inference不一致"] G -- Yes --> I[キャッシュヒット] D --> J[フルトークン課金] F --> J H --> J I --> K["キャッシュ読み出し課金
通常の1/10"] J --> L[レスポンス返却] K --> L style D fill:#ff6b6b,color:#fff style F fill:#ff6b6b,color:#fff style H fill:#ff6b6b,color:#fff style I fill:#51cf66,color:#fff style J fill:#ffa94d,color:#fff style K fill:#74c0fc,color:#fff
このフローが示すのは、ユーザーがどんなに正しく設定しても、3つのサーバー側チェックポイントを全部通過しないとキャッシュは効かないということ。90%がサーバー側原因というデータは、この3つのどれかで大半が弾かれていることを意味する。
promptCacheBreakDetection.ts の構造推測
流出したファイル名と内部コメントから、このコードの役割を推測できる(以下はあくまで公開情報から類推した構造であり、公式確認はない)。
Anthropicが内部でキャッシュ破壊を検出・分類するために使用していたシステムと考えられ、BigQueryのデータパイプラインと連携して稼働していたと推測される。
// 以下は流出コメントと公開情報から推測した構造(非公式・参考用)
// 実際のAnthropicの実装とは異なる可能性がある
enum CacheBreakReason {
SERVER_ROUTING = "server_routing", // サーバーへのルーティング変更
CACHE_EVICTION = "cache_eviction", // キャッシュ削除(TTL超過・LRU等)
BILLING_INFERENCE_DISAGREEMENT = "billed_inference_disagreement", // 課金・推論判定不一致
UNKNOWN = "unknown",
}
interface CacheBreakEvent {
requestId: string;
timestamp: Date;
reason: CacheBreakReason;
expectedCacheTokens: number;
actualCacheTokens: number;
model: string;
}
// PR #19823 のコメント示唆: ~90%はサーバー側 (ROUTING + EVICTION + DISAGREEMENT)
// BigQueryのクエリでサーバー側起因かどうかを判定していたと推測
function classifyBreakReason(event: CacheBreakEvent): CacheBreakReason {
if (event.actualCacheTokens === 0 && event.expectedCacheTokens > 0) {
// ルーティング起因 or Eviction起因の判定ロジック(推測)
if (isRoutingChange(event)) return CacheBreakReason.SERVER_ROUTING;
if (isCacheEvicted(event)) return CacheBreakReason.CACHE_EVICTION;
if (isBillingMismatch(event)) return CacheBreakReason.BILLING_INFERENCE_DISAGREEMENT;
}
return CacheBreakReason.UNKNOWN;
}
「課金とキャッシュ判定の不一致」という項目が存在したという事実は重い。これはAnthropicの課金システムと実際の推論処理が、独立したシステムとして動作しており、両者の間に整合性の問題が生じていたことを示唆している。
それでもできる対策 — ユーザー側の10%を潰す
90%はAnthropic側の問題だが、残り10%は自分のコードで改善できる。さらに、サーバー側の問題を「起きにくくする」工夫もある。
1. キャッシュ対象を安定したコンテンツに絞る
キャッシュは「変化しない部分」に適用するのが原則だ。リクエストごとに変わる情報をキャッシュ対象にすると、毎回キャッシュミスになる。
# 悪い例:変化する情報をキャッシュ対象にしている
bad_system = [
{
"type": "text",
"text": f"現在時刻: {datetime.now()}。あなたはアシスタントです...",
"cache_control": {"type": "ephemeral"}, # タイムスタンプが変わるためキャッシュは毎回ミス
}
]
# 良い例:不変のコンテンツのみキャッシュ対象にする
good_system = [
{
"type": "text",
"text": "あなたは優秀なアシスタントです。以下の製品マニュアルを参照して回答してください。\n\n" + large_manual_text,
"cache_control": {"type": "ephemeral"}, # マニュアルは変化しないのでキャッシュ効果が高い
}
]
2. キャッシュ有効期限内にリクエストを集中させる
Anthropicのキャッシュ有効期限は最長5分とされている(公式ドキュメントより)。バッチ処理を行う場合は、この5分の窓内にリクエストを集中させることでヒット率が上がる。
3. キャッシュ対象のトークン数を最小キャッシュ可能サイズ以上に保つ
Claude 3.5 Sonnet / Claude Opus 4.5 では、キャッシュ可能な最小トークン数は1024トークン。これを下回るコンテンツへのキャッシュ指定は無効になる。
4. マルチターンの会話では会話履歴もキャッシュする
# 長い会話履歴のキャッシュ例
messages = [
{"role": "user", "content": "最初の質問"},
{"role": "assistant", "content": "最初の回答"},
# ...多数のターン...
{
"role": "user",
"content": [
{
"type": "text",
"text": "ここまでの会話を踏まえて...",
"cache_control": {"type": "ephemeral"}, # 会話履歴全体をキャッシュ
}
]
}
]
Claude Code Auto Modeの解説記事でも触れているが、Claude系APIを活用する際のコスト最適化において、プロンプトキャッシュは最も費用対効果の高い手法の一つだ。
なぜこれが問題なのか — 「壊れた理由」が見えない
今回の流出が示す本質的な問題は、キャッシュが壊れた理由をユーザーが知る手段がないことだ。APIのレスポンスには cache_read_input_tokens: 0 とだけ表示され、なぜキャッシュがミスしたのかは一切わからない。
一方でAnthropicは内部的には破壊の原因を分類・追跡するシステム(promptCacheBreakDetection.ts)を持っていた。この情報の非対称性は、ユーザーが「自分の実装が悪いのか、サーバー側の問題なのか」を判断できない状況を生んでいた。
課金不一致の示唆
「課金とキャッシュ判定の不一致(billed/inference disagreement)」という記述は、単なる技術的バグの記録ではない可能性がある。キャッシュがヒットしていないのにキャッシュ生成料金が発生した、あるいはその逆のケースがあったとすれば、課金の正確性に関する問題提起になる。ただし、Anthropicからの公式説明はなく、流出コメントの文脈の詳細は不明だ。
競合APIとの比較
OpenAIもGemini APIも、プロンプトキャッシュ相当の機能(OpenAIはPrompt Caching、GeminiはContext Caching)を提供しているが、キャッシュ破壊率の内部データを公開しているAPIプロバイダーは現時点で存在しない。今回の流出は、業界全体としてキャッシュの信頼性に関する透明性向上が求められていることを示している。
AIエージェントやコーディングアシスタントの構築にOpenHandsやLangChainを活用している開発者にとっても、Claude APIのキャッシュ信頼性はワークフロー全体のコスト予測に直結する問題だ。
注記
本記事は、X上の報告に基づく速報である。Anthropicからの公式声明はまだ発表されていない。技術的詳細の一部は流出コード分析および公開ドキュメントに基づく推測を含む。promptCacheBreakDetection.tsの具体的な実装詳細は公式確認がなく、本記事のコード例は参考目的の推測である。
参照ソース
この記事はAI業界の最新動向を速報でお届けする「AI Heartland ニュース」です。