X上で、Claude Codeの流出コードからpromptCacheBreakDetection.tsというファイルが発見された。このファイルの内部コメント(PR #19823関連)に、キャッシュ破壊現象に関するBigQueryベースの分析結果が記録されていた。
流出情報の要点:
“~90% of breaks are server-side routing/eviction or billed/inference disagreement”
このコメントは、プロンプトキャッシュの破壊事象の約90%がサーバー側要因(ルーティング、削除、課金判定の不一致)で発生していることを示している。ユーザー側の実装ミスは10%以下に過ぎない。
Anthropicからの公式確認・声明はまだ発表されていない。以下の技術的分析は、流出コードおよびAnthropicの公開ドキュメントをもとにした推測を含む。
Claude APIのプロンプトキャッシュ(Prompt Caching)は、長大なコンテキストを繰り返し送信するコストとレイテンシを削減するための機能だ。仕組みは単純で、リクエストのプレフィックス部分をAnthropicのサーバー側で保持しておき、次回リクエスト時に再利用する。
公式ドキュメントによれば、キャッシュは以下の条件で機能する。
cache_control: {type: "ephemeral"} をメッセージブロックに付与する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}")
キャッシュ破壊(Cache Break)とは、前回リクエストでキャッシュを生成したにもかかわらず、次回リクエストでキャッシュヒットせず、再度フルトークン課金が発生する事象だ。
流出コメントの分析から、破壊の原因は大別して3種類と推定される(公式確認なし)。
1. サーバー側ルーティングの問題 Anthropicのインフラでは、リクエストは複数のサーバーにルーティングされる。キャッシュは特定のサーバー(またはサーバーグループ)に紐付いているため、別サーバーにルーティングされた場合はキャッシュが存在しない状態になる。
2. キャッシュ削除(Eviction) 5分の有効期限内でも、サーバー側のメモリ圧力やLRUポリシーによってキャッシュが削除されることがある。特にトラフィックが集中する時間帯には発生しやすい。
3. 課金とキャッシュ判定の不一致(Billed/Inference Disagreement) 最も興味深い要因がこれだ。課金システムがキャッシュヒットを記録した一方で、実際の推論エンジンはキャッシュを使用しなかった(あるいはその逆)ケースが存在した可能性がある。これは二重の問題を意味する。コスト計算が実際の処理と乖離していたリスクだ。
Claude APIのキャッシュ料金体系(2026年3月時点の公式ドキュメントより)は以下の通りだ。
| トークン種別 | 料金(claude-opus-4-5の場合) |
|---|---|
| 通常入力トークン | $15 / 1Mトークン |
| キャッシュ生成トークン | $18.75 / 1Mトークン(通常の1.25倍) |
| キャッシュ読み出しトークン | $1.50 / 1Mトークン(通常の1/10) |
キャッシュが正常に機能した場合と機能しなかった場合では、コストに大きな差が生まれる。
前提条件の例:
# キャッシュ効果のコスト計算スクリプト
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()
このスクリプトを本番環境に組み込むことで、実際のキャッシュヒット率を継続的にモニタリングできる。ヒット率が期待値を大幅に下回る場合は、今回流出した情報が示すサーバー側の問題が発生している可能性を疑うべきだ。
flowchart TD
A[APIリクエスト送信] --> B{Anthropicサーバーへのルーティング}
B --> C{前回と同じサーバー<br/>グループか?}
C -- No --> D[キャッシュミス<br/>ルーティング起因]
C -- Yes --> E{キャッシュが<br/>有効期限内か?}
E -- No --> F[キャッシュミス<br/>Eviction起因]
E -- Yes --> G{課金システムと<br/>推論エンジンが<br/>一致するか?}
G -- No --> H[キャッシュミス<br/>Billed/Inference不一致]
G -- Yes --> I[キャッシュヒット]
D --> J[フルトークン課金]
F --> J
H --> J
I --> K[キャッシュ読み出し課金<br/>通常の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%がサーバー側起因という流出データは、このフローのいずれかが大多数のキャッシュ破壊を引き起こしていることを示す。
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の課金システムと実際の推論処理が、独立したシステムとして動作しており、両者の間に整合性の問題が生じていたことを示唆している。
サーバー側の問題が90%を占めるとしても、残り10%はユーザー側の実装に起因する。また、サーバー側の問題を軽減するための実装上の工夫も存在する。
キャッシュは「変化しない部分」に適用するのが原則だ。リクエストごとに変わる情報をキャッシュ対象にすると、毎回キャッシュミスになる。
# 悪い例:変化する情報をキャッシュ対象にしている
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"}, # マニュアルは変化しないのでキャッシュ効果が高い
}
]
Anthropicのキャッシュ有効期限は最長5分とされている(公式ドキュメントより)。バッチ処理を行う場合は、この5分の窓内にリクエストを集中させることでヒット率が上がる。
Claude 3.5 Sonnet / Claude Opus 4.5 では、キャッシュ可能な最小トークン数は1024トークン。これを下回るコンテンツへのキャッシュ指定は無効になる。
# 長い会話履歴のキャッシュ例
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からの公式説明はなく、流出コメントの文脈の詳細は不明だ。
OpenAIもGemini APIも、プロンプトキャッシュ相当の機能(OpenAIはPrompt Caching、GeminiはContext Caching)を提供しているが、キャッシュ破壊率の内部データを公開しているAPIプロバイダーは現時点で存在しない。今回の流出は、業界全体としてキャッシュの信頼性に関する透明性向上が求められていることを示している。
AIエージェントやコーディングアシスタントの構築にOpenHandsやLangChainを活用している開発者にとっても、Claude APIのキャッシュ信頼性はワークフロー全体のコスト予測に直結する問題だ。
本記事は、X上の報告に基づく速報である。Anthropicからの公式声明はまだ発表されていない。技術的詳細の一部は流出コード分析および公開ドキュメントに基づく推測を含む。promptCacheBreakDetection.tsの具体的な実装詳細は公式確認がなく、本記事のコード例は参考目的の推測である。
この記事はAI業界の最新動向を速報でお届けする「AI Heartland ニュース」です。