How Claude's prompt cache keys a request, what the cache read and cache write token fields count, and what a cache miss costs.
Verified 2026-09-04: three independent investigations (/investigate, run wf_6d36f04f-246) merged adversarially against primary sources and jq-mined local transcripts, recomputing a review-skill run whose tokens exploded. FACT: primary source or reproduced. HYPOTHESIS: unproven; the runbook measures it. Session-level numbers: Token usage.
| Source | Used for |
|---|---|
| Prompt-caching docs | Breakpoints, keys, TTL, invalidation table. |
| Messages API reference | usage field semantics. |
| Claude Code costs, Claude Code prompt caching | How plan usage draws on cache reads. |
| API rate limits | Cache-aware ITPM (input tokens per minute). |
| Local transcripts | ~/.claude/projects/**/*.jsonl, mined with jq. |
| The review-skill run | ~/Downloads/d3337a3f-a576-4040-a271-29ef4474e801.jsonl plus its subagents/. |
The Messages API is stateless: Anthropic keeps no session. Every call resends the whole conversation in order (tool definitions, system prompt, all messages), and the model processes all of it before generating a token. A 60-turn Claude Code session processes turn 1's content ~60 times, turn 2's ~59; uncached, each pass pays full input price.
Prompt caching stores the processed state of a request's beginning, the prefix; the next request computes only the new end, the suffix.
request N: [tools][system][msg1][msg2]...[msgK]
└────────── prefix ─────────┘
request N+1: [tools][system][msg1][msg2]...[msgK][asst reply][new msg]
└── identical to request N, cached ──┘└─ new suffix ──┘
FACT From the prompt-caching docs.
cache_control marks a breakpoint, at most 4 per request. Claude Code sets them; you never do.
The server hashes everything from the request's start to each breakpoint: tools, system, messages, in order, byte for byte. The hash is the cache key; it stores key → processed model state.
The next request matches the longest stored key equal to its beginning, checking breakpoint positions backward within a ~20-position lookback window. Everything up to the match comes from cache.
| Rule | Detail |
|---|---|
| Matches only from the start | A change at byte 3,000 of the system prompt breaks every key over positions ≥ 3,000, though 99% of the content is unchanged. |
| Not session-scoped | Same starting bytes from your account hit the same entries. Observed: a new session's first call read cache_read = 45,383 that a parallel session in the same directory (same system prompt and CLAUDE.md) wrote minutes earlier. |
| Minimum size | 512 tokens; shorter prompts are silently uncached. |
| TTL | 5 minutes on API keys; 1 hour on subscriptions (Pro/Max/Team), 5m once on usage credits. |
| Refresh | A hit refreshes the TTL for free; an active session never expires its own cache. |
FACT Each response's usage splits the request into three input buckets plus output (docs semantics, verbatim).
| Field | Counts | Price (× base input) |
|---|---|---|
| cache_read_input_tokens | The matched prefix, served from cache. | 0.1× |
| cache_creation_input_tokens | Processed fresh and written to cache, up to the last breakpoint. | 1.25× (5m TTL) / 2× (1h TTL) |
| input_tokens | Leftover after the last breakpoint: fresh, uncached. | 1× |
| output_tokens | Generated tokens; thinking counts as output. | 5× |
input_tokens is not the prompt size. Full context per call: context = input_tokens + cache_creation_input_tokens + cache_read_input_tokens.
Claude Code puts a breakpoint at essentially every request's end, so the whole conversation lands in the two cache buckets; ~2 tokens of structural wrapper remain. FACT input_tokens ≤ 2 on all ~470 sampled Claude Code calls.
A raw API call without a breakpoint at the end (no cache_control) reports the whole prompt as input_tokens, both cache fields 0. In Claude Code the new suffix (new message, previous assistant reply, new tool results) bills once as cache_creation, then as cache_read on every later call.
A fresh Claude Code session; system prompt, CLAUDE.md and tool definitions total ~5,000 tokens.
Request [5,000 system/tools][100 user], cold, breakpoint at the end. Stores a 5,100-token key over system, tools and question.
usage: input=2, cache_creation=5,100, cache_read=0, output=300
Resent with the 300-token assistant reply (the tool call) and a 400-token tool result appended. Call 1's key matches: 5,100 served at 0.1×, the new 700 processed fresh, a new key covers 5,800.
usage: input=2, cache_read=5,100, cache_creation=700, output=250
cache_read=5,800, cache_creation=1,250 (prev output 250 + result 1,000), input=2
| Claim | Evidence | Status |
|---|---|---|
cache_read(N+1) = cache_read(N) + cache_creation(N) | Exact on 268/285, 51/52, 129/130 and 32/32 consecutive calls across four transcripts. | FACT |
session cache_read total ≈ Σ over calls (context size at that call) | Review-skill run: Σ cache_read 60.84M vs Σ per-call context 63.5M, a 95.7% match. | FACT |
Each token is written once, on its first turn, and read on every later call: at 200k context every call, even "say ok", pays ~200k cache-read tokens.
Same session, call 40, context 115,000 tokens. Between calls 39 and 40 you toggle a hook, changing the system prompt at about token 2,000. Every long key hashed the old bytes; the longest surviving match is a breakpoint before the change, perhaps the tools block.
FACT Real event from a local transcript, 2026-07-14T19:04:24:
call 39: cache_read=114,903 cache_creation=1,850 input=2 ← normal call 40: cache_read=32,852 cache_creation=88,743 input=2 ← MISS call 41: cache_read=121,595 cache_creation=... input=2 ← healed (32,852+88,743 exactly)
Call 40 read the 32,852 tokens before the change at 0.1× and re-wrote the other 88,743 (everything from the changed byte on, messages unchanged) at the 2× write rate. Misses never bill at base rate or show in input_tokens, which stays 2. Call 41 resumes the recurrence on the rebuilt cache.
| Claim | Evidence | Status |
|---|---|---|
| A miss turns the turn's full-context 0.1× read into a partial or full 2× write: up to a ~20× cost spike for that turn. | Worst observed: 791,843 tokens re-written in one call, cache_read down to 36,106: ≈ 1.58M input-token equivalents, dozens of normal turns. | FACT |
FACT The invalidation hierarchy is tools → system → messages: a change invalidates its level and everything after.
| Miss trigger | Level |
|---|---|
| Tool set changes mid-session: an MCP server connects or disconnects, ToolSearch loads a tool | tools |
| Hooks toggled, settings edits, CLAUDE.md reload | system |
| Compaction rewrites the whole message list | messages: full rebuild |
| Idle past the TTL | all: entries expired, the next call re-writes the whole context at write rates. Subscription main conversation: 1h. Subagents, workflows and compaction: 5m unless subagentPromptCacheTtl is set, so a slow fan-out can quietly miss. |
FACT Per the docs' "What invalidates the cache" table (fetched 2026-09-04), the thinking configuration (mode on/off, budget_tokens) is rendered into the prompt. Changing it invalidates the messages-level cache, plus tools and system on models that render it ahead of them. Messages are almost all of a long session's context: treat a mid-session thinking or effort change as a near-full-context 2×-rate rebuild on the next call.
Same for the speed setting (system and message caches), tool_choice and image changes (message blocks).
FACT Thinking blocks cache like other content and count as input when read back. Opus 4.5+ and Sonnet 4.6+ keep previous-turn thinking blocks by default, so a normal user message keeps the cache valid. Older Opus/Sonnet and all Haiku strip prior thinking blocks on non-tool-result user content: the bytes change, invalidating from there.
The docs are silent on whether entries are model-specific. Measured: claude-fable-5 and claude-opus-4-8 share them, on two transcripts with mid-session /model-style switches (8d6cc0a1 in the smine worktree, 5f9a7ba3 in remove-agent-worktree-bug).
fable-5: in=2 cw=687 cr=187,489 ← last fable call, context ~188k --- switch --- opus-4-8: in=2 cw=0 cr=162,961 ← first opus call READ 163k from opus-4-8: in=2 cw=18,659 cr=162,961 cache written by fable, wrote 0 --- switch back --- fable-5: in=2 cw=23,317 cr=190,576 ← fable read opus's writes back
The read prefix holds the session's own unique messages, so only the other model can have written it: cross-model reads in both directions.
| Claim | Evidence | Status |
|---|---|---|
| For this model pair a switch is a partial miss, not a full rebuild. | ~87–96% of the context survived; the ~13–25k rebuilt at write rates fits messages-level invalidation (stripped thinking blocks, re-rendered config in recent turns). | FACT |
| This generalizes to other model pairs. | Unmeasured. Across model families or tokenizers a switch may be a full-context rebuild; budget for one. | HYPOTHESIS |
The full rebuild 2.4h after a switch in 8d6cc0a1 was TTL expiry, not the model change.
FACT Opus 5 prices.
| Bucket | Price per MTok |
|---|---|
| Base input | $5 |
| 5m cache write | $6.25 |
| 1h cache write | $10 |
| Cache read | $0.50 |
| Output | $25 |
FACT Fable/Mythos 5.1 reads cost 0.025× ($0.125), 4× cheaper than the 0.1× of non-.1 models.
Staleness: cost conclusions frozen at 0.1× reads go stale on 5.1-generation models, where 1h writes clearly dominate weighted cost.
Two systems meter cache tokens differently.
| Claim | Evidence | Status |
|---|---|---|
API rate limits: input_tokens and cache_creation count toward ITPM, cache_read does not (except Haiku 3.5). Cache-read growth is rate-limit free; a miss costs twice: 2×-rate billing and full ITPM. | Rate-limits page, "Cache-aware ITPM". | FACT |
| Subscription 5h and weekly windows: cache reads draw plan usage, at the cached token rate. | Claude Code costs doc: a one-line question in a long-open session "still draws usage for the whole conversation". /usage flags long-context and cache-miss behavior at ≥10% of recent usage. | FACT |
| The formula mapping the four fields to window drain. | Unpublished; pricing-multiplier weighting is a proxy, not documented arithmetic. The runbook measures it. | HYPOTHESIS |
| Rule | Evidence | Status |
|---|---|---|
Dedup by message.id | The JSONL repeats a response's usage on each content-block row; per-row sums inflate 1.84×–3.2×. | FACT |
Take the per-id max of output_tokens | Subagent transcripts stream output: an id's early rows carry partial values (1 → … → 548). First-row dedup undercounted the review-skill run's subagent output 4.7×. | FACT |
Ignore the iterations array | Duplicates the top-level usage numbers. | FACT |
jq -s '[group_by(.message.id)[] | map(.message.usage) | {
in: .[0].input_tokens, cw: .[0].cache_creation_input_tokens,
cr: .[0].cache_read_input_tokens, out: (map(.output_tokens) | max)}] |
{calls: length, input: (map(.in)|add), cache_write: (map(.cw)|add),
cache_read: (map(.cr)|add), output: (map(.out)|add)}' \
<(jq -c 'select(.message.usage != null and .message.id != null)' SESSION.jsonl)
The peeked diagnosis plan's claims, checked against the transcripts.
| Claim | Evidence | Status |
|---|---|---|
| Every call re-reads the full accumulated context as cache_read | Exact recurrence identity (cache hits). | HOLDS |
| cache_read ≈ Σ(context × calls) | Accurate to ~4–5%. | HOLDS |
| Dedup usage by message.id | Plus the subagent per-id max for output (mining). | HOLDS |
| Write-once / read-many per turn; input_tokens small | ~2 per call (usage fields). | HOLDS |
| Run totals 541 calls / 60.8M read / 2.70M write / 575k output | Reproduced exactly from the raw transcripts in ~/Downloads/d3337a3f-*, not ~/.claude/projects. | HOLDS |
| "165 API calls" for the orchestrator | Internally inconsistent; the transcripts show 80 main-session calls. | REFUTED |
| TTL framing "expiry >5 min" | 1h on a subscription's main conversation; 5m for subagents and compaction (cache miss). | CORRECTED |
| "Output ruled out at <1% of read volume" | As a cost statement it compares categories priced 50× apart. Cost-weighted: ~42% reads ($30.4-equiv), ~37% writes ($27.0), ~20% output ($14.4). Pre-429, reads were 94.6% of raw tokens, ~49% cost-weighted. | REFUTED |
| "Cache-read is what exhausts the 5-hour window" | Reads draw plan usage (FACT). Raw-volume metering (reads dominate) vs price-weighted (reads ≈ writes ≈ half) is unmeasured; the runbook settles it. | DOWNGRADED to directional |
| Architectural root cause: a context-accumulating orchestrator plus 17 re-grounding subagents | Holds under either weighting; attribution softens to context re-read compounding plus 2×-rate rebuilds plus output, together. | HOLDS |
Also refuted: "cache hits don't count against your limit" holds only for API ITPM, not plan windows; a fresh session need not start cold (cache keys); misses never bill as base-rate input_tokens.
Does the Team-plan 5-hour window drain per raw or per price-weighted token? Each cell isolates one variable in a fresh 5-hour window: read /usage before and after, compute drain per token from the transcript, deduped as in Mining.
Fresh window, zero activity, same wall clock as cell 1. Bounds /usage display drift.
Build ~150k context (paste a large file), then loop ~30 one-liners ("say ok"): each re-reads ~150k with minimal write and output, ~5M cache_read in total. The /usage delta gives drain per cache_read token. Transcript:
ls -t ~/.claude/projects/<project-dir>/*.jsonl | head -1
Fresh window, same volume as truly uncached input: unique content per turn so no prefix matches, or prompts under the 512-token floor. Wall clock and output matched; decision rule pre-registered below.
Force misses: toggle a hook (system-prompt change) several times in a large-context session, each a 2×-rate rebuild. Compare drain per write token with cell 1's per read token; pricing predicts ~20×.
Small stable prompt, long generations. Drain per output token; pricing predicts ~5× the input rate.
Subagent TTL: fan out Task subagents without subagentPromptCacheTtl, check each subagent transcript's ephemeral_5m vs ephemeral_1h split (docs predict 5m); repeat with subagentPromptCacheTtl: "1h", confirm the flip. Main-session TTL: idle 50 min → full cache_read (1h intact); idle 70 min → full-context cache_creation rebuild.
jq -s '[.[] | select(.message.usage.cache_creation) | .message.usage.cache_creation] |
{m5: (map(.ephemeral_5m_input_tokens)|add), h1: (map(.ephemeral_1h_input_tokens)|add)}' \
<(jq -c 'select(.message.usage != null)' SESSION.jsonl)| Cell 1 vs cell 2 outcome | Conclusion |
|---|---|
| Drain per cache_read token ≈ drain per fresh input token | Raw-count metering: cache-read is the window killer at volume. |
| Cell 1 ~10× cheaper per token (~40× on a 5.1 model) | Pricing-mirror weighting: writes and output matter as much as reads. |
Re-invoke the investigation with resumeFromRunId: wf_6d36f04f-246 to make results binding facts. The fan-out replays from cache; only the merge re-runs.
subagentPromptCacheTtl; subagents default to a 5m TTL and can miss on slow fan-outs.message.id, per-id max for output_tokens, ignore iterations.