The live cache autopsy: the cache, proven on your own machine
TL;DR. Every transcript Claude Code writes contains the API's own per-call usage block,
which makes your disk a complete record of what the prompt cache did on every turn you ever
ran. This chapter dissects one real 272-call working session from this repository: 97.5%
cache hit rate, $110.68 spent where no-cache would have been $680.83 (6.2x), every write
bought at the 1-hour TTL, a peak prompt of 960k tokens, and exactly 6 invalidation events,
five of them TTL lapses from walking away, which together re-wrote 2.06M tokens at 2x and cost
about $19.61 that staying warm would have made $1.03. Then the chapter turns each finding into
a proof session: short, exact claude -p experiments that demonstrate cache existence,
TTL expiry, invalidation-by-edit, and the /clear-versus-/compact difference on your own
transcripts, so no cache claim in this book has to be taken on faith.
Contents
- The instrument you already have
- The autopsy
- Reading the lifecycle
- The price of walking away
- The proof sessions
- Further reading
- Takeaways
Chapter 43 gave the ideas and the people; this chapter is the evidence. Chapter 24 derived the cache rules from constructed prompts and Chapter 25 built the whole-machine ledger; what has been missing is the middle scale: one session, call by call, watching the cache warm up, serve, and break. That is an autopsy, and the body is already on your disk.
The instrument you already have
Claude Code logs every session to ~/.claude/projects/<project>/<session-id>.jsonl, and every
assistant line carries the message.usage block the API returned: input_tokens (full
price), cache_creation_input_tokens (split by TTL under cache_creation),
cache_read_input_tokens (0.1x), output_tokens. Two parsing facts matter and the script
handles both: a message id repeats once per content block with identical usage (deduplicate on
id), and subagent sidechain lines are marked so they can be excluded. The result is a per-API-
call cache history nobody had to instrument for: the transcript is the instrument.
The autopsy
"""The cache autopsy: one real session's cache lifecycle, from the transcript.
Chapter 25 built a ledger over every transcript on the machine; this script
goes the other way and dissects ONE session, API call by API call, to show the
prompt cache doing its job and occasionally losing it:
cold start the first call writes the whole prompt to cache
extend the normal turn: the old prefix is read at 0.1x, only the new
chunk is written
invalidation cache_read DROPS while cache_creation spikes: the prefix
changed upstream (tool list, CLAUDE.md, system), the TTL
lapsed while you were away, or the context was compacted
It also settles which TTL Claude Code buys (the usage block splits writes into
ephemeral_5m and ephemeral_1h) and prices the session against a no-cache
counterfactual.
Run: python3 cache_autopsy.py [transcript.jsonl]
Default: the largest transcript for this repo's project directory.
Standard library only. The format is internal to Claude Code; the parser
reads only message.usage (the API's own response shape) and skips the rest.
"""
import json
import re
import sys
from datetime import datetime
from pathlib import Path
PRICES = {"claude-opus-4-8": (5.00, 25.00), "claude-opus-4-7": (5.00, 25.00),
"claude-sonnet-5": (3.00, 15.00), "claude-sonnet-4-6": (3.00, 15.00),
"claude-haiku-4-5": (1.00, 5.00), "claude-fable-5": (10.00, 50.00)}
def pick_default():
"""Claude Code munges the project cwd into a directory name; find the
current repo's transcripts and take the biggest session.
Walks up from the cwd, because you usually run this from a subdirectory
(this file lives in code/) while the transcripts are keyed on the project
root you launched Claude Code from.
"""
for d in [Path.cwd(), *Path.cwd().parents]:
root = Path.home() / ".claude/projects" / re.sub(r"[/.]", "-", str(d))
sessions = list(root.glob("*.jsonl"))
if sessions:
return max(sessions, key=lambda p: p.stat().st_size)
sys.exit(
"no Claude Code transcripts found for this directory or any parent.\n"
"pass one explicitly:\n"
f" python3 {Path(__file__).name} ~/.claude/projects/<project>/<session>.jsonl"
)
def load_calls(path):
"""One entry per API call: transcripts repeat a message id once per
content block, with identical usage, so dedup on id keeping the first."""
calls, seen = [], set()
for line in open(path, errors="replace"):
try:
d = json.loads(line)
except json.JSONDecodeError:
continue
if d.get("type") != "assistant" or d.get("isSidechain"):
continue
m = d.get("message") or {}
u, mid = m.get("usage"), m.get("id")
if not u or mid in seen:
continue
seen.add(mid)
w = u.get("cache_creation") or {}
calls.append(dict(
ts=datetime.fromisoformat(d["timestamp"].replace("Z", "+00:00")),
model=m.get("model", "?"),
inp=u.get("input_tokens", 0),
w5=w.get("ephemeral_5m_input_tokens", 0),
w1=w.get("ephemeral_1h_input_tokens", 0),
cc=u.get("cache_creation_input_tokens", 0),
cr=u.get("cache_read_input_tokens", 0),
out=u.get("output_tokens", 0)))
return calls
def classify(i, c, prev, ttl_s):
if i == 0:
return "cold start"
gap = (c["ts"] - prev["ts"]).total_seconds()
prompt, prev_prompt = c["inp"] + c["cc"] + c["cr"], prev["inp"] + prev["cc"] + prev["cr"]
if c["cr"] < prev["cr"] and c["cc"] > 1000:
if gap > ttl_s:
return f"INVALIDATION: TTL lapsed ({gap/60:.0f} min gap)"
if prompt < prev_prompt * 0.6:
return "INVALIDATION: context shrank (compact/clear)"
return "INVALIDATION: prefix changed upstream"
return "extend"
path = Path(sys.argv[1]) if len(sys.argv) > 1 else pick_default()
calls = load_calls(path)
model = calls[0]["model"]
w5, w1 = sum(c["w5"] for c in calls), sum(c["w1"] for c in calls)
ttl_s = 3600 if w1 >= w5 else 300
print(f"=== Cache autopsy: {path.name[:23]}... ===")
print(f"{len(calls)} API calls, {model}, "
f"{calls[0]['ts']:%Y-%m-%d %H:%M} -> {calls[-1]['ts']:%Y-%m-%d %H:%M} UTC")
print(f"cache writes bought: 5m TTL {w5:,} tok, 1h TTL {w1:,} tok "
f"-> this session runs on the {'1-hour' if ttl_s == 3600 else '5-minute'} cache\n")
print(f"{'call':>4} {'gap':>6} {'input':>7} {'write':>8} {'read':>9} {'out':>6} event")
print("-" * 78)
events, shown = [], 0
for i, c in enumerate(calls):
ev = classify(i, c, calls[i - 1] if i else None, ttl_s)
if "INVALID" in ev or i == 0:
events.append((i, ev))
if i < 8 or "INVALID" in ev:
shown += 1
gap = f"{(c['ts'] - calls[i-1]['ts']).total_seconds():>5.0f}s" if i else " -"
print(f"{i:>4} {gap} {c['inp']:>7,} {c['cc']:>8,} {c['cr']:>9,} {c['out']:>6,} {ev}")
print(f"{'...':>4} ({len(calls) - shown} extend calls not shown)")
print("-" * 78)
prompt = sum(c["inp"] + c["cc"] + c["cr"] for c in calls)
read = sum(c["cr"] for c in calls)
inp_price, out_price = PRICES.get(model, (5.0, 25.0))
actual = sum(c["inp"] * inp_price + c["w5"] * inp_price * 1.25
+ c["w1"] * inp_price * 2.00 + c["cr"] * inp_price * 0.10
+ c["out"] * out_price for c in calls) / 1e6
uncached = sum((c["inp"] + c["cc"] + c["cr"]) * inp_price
+ c["out"] * out_price for c in calls) / 1e6
peak = max(c["inp"] + c["cc"] + c["cr"] for c in calls)
print(f"\nPrompt tokens processed: {prompt:,} (peak single prompt: {peak:,})")
print(f"Served from cache: {read:,} ({read / prompt * 100:.1f}% hit rate)")
print(f"Invalidation events: {len(events) - 1} in {len(calls)} calls")
print(f"Session cost: ${actual:,.2f}")
print(f"Same session, no cache: ${uncached:,.2f} "
f"(caching saved {uncached / actual:.1f}x)")
print(f"""
Reading it: the steady state is 'extend' (old prefix read at 0.1x, new chunk
written once). Every INVALIDATION line is money: the next call re-writes what
was already cached, at {'2.0x' if ttl_s == 3600 else '1.25x'} instead of reading it at 0.1x. Match each one
to what you did at that moment (edited CLAUDE.md? changed MCP servers? walked
away past the TTL? compacted?) and you have the chapter's rules, proven on
your own bill.""")
Run against the largest completed session in this repository's project directory (the two-day session that wrote an earlier part of this book), verified output:
=== Cache autopsy: c778f680-f439-41db-b0d8... ===
272 API calls, claude-opus-4-8, 2026-06-29 01:29 -> 2026-06-30 18:43 UTC
cache writes bought: 5m TTL 0 tok, 1h TTL 3,198,587 tok -> this session runs on the 1-hour cache
call gap input write read out event
------------------------------------------------------------------------------
0 - 6,199 6,052 15,853 318 cold start
1 9s 2 7,250 21,905 478 extend
2 7s 2 4,991 29,155 449 extend
3 12s 2 5,219 34,146 810 extend
4 23s 124 9,089 39,365 1,675 extend
5 38s 2 35,206 15,946 1,889 INVALIDATION: prefix changed upstream
6 33s 2 7,068 51,152 3,194 extend
7 23s 124 5,448 58,220 195 extend
80 82496s 46 210,309 15,946 4,047 INVALIDATION: TTL lapsed (1375 min gap)
96 4759s 2 248,554 15,946 9,989 INVALIDATION: TTL lapsed (79 min gap)
157 31625s 124 751,426 15,946 3,525 INVALIDATION: TTL lapsed (527 min gap)
209 4147s 1,086 391,599 21,169 4,814 INVALIDATION: TTL lapsed (69 min gap)
227 7206s 2 462,398 21,169 4,790 INVALIDATION: TTL lapsed (120 min gap)
... (259 extend calls not shown)
------------------------------------------------------------------------------
Prompt tokens processed: 133,558,463 (peak single prompt: 960,535)
Served from cache: 130,253,349 (97.5% hit rate)
Invalidation events: 6 in 272 calls
Session cost: $110.68
Same session, no cache: $680.83 (caching saved 6.2x)
Reading it: the steady state is 'extend' (old prefix read at 0.1x, new chunk
written once). Every INVALIDATION line is money: the next call re-writes what
was already cached, at 2.0x instead of reading it at 0.1x. Match each one
to what you did at that moment (edited CLAUDE.md? changed MCP servers? walked
away past the TTL? compacted?) and you have the chapter's rules, proven on
your own bill.
Reading the lifecycle
Walk the table top to bottom and every rule from the caching chapters appears in the wild:
- Call 0, the cold start, is not fully cold. The session's first call already reads 15,853 tokens. That is the shared spine (system prompt and tool schemas) still warm from earlier traffic on the same account, the fleet effect Chapter 43's radix lab predicted: your sessions are branches of one tree, and the trunk was already paid for.
- The steady state is beautiful and boring. Calls 1 to 4:
inputis nearly zero, the read column climbs by exactly the previous turn's write (21,905, then 29,155, then 34,146...), and the write column is just each turn's new chunk. This is incremental prefix caching working as designed: the conversation only ever pays full attention cost for what is new. - Call 5 is a textbook upstream invalidation. The read column crashes from 39,365 back to 15,946 (the shared spine) and the write column spikes to 35,206: everything above the spine was re-written. Something in the early prompt changed, 38 seconds after the previous call, which no TTL explains; this is the shape of a prefix edit (Chapter 24's silent-invalidator audit, observed in production).
- Claude Code buys the 1-hour cache. All 3.2M tokens of writes in this session are
ephemeral_1h: the tool pays 2x on writes (instead of 1.25x for the 5-minute tier) to keep a working session warm through pauses, tool runs, and thinking time. For an interactive agent whose turns can be many minutes apart, that trade is obviously right, and the five TTL lapses below show what happens at the boundary where even an hour is not enough. - The peak prompt is 960k tokens. This session lived deep in the 1M window, which is exactly where caching stops being an optimization and becomes the only thing making the session affordable: at $5/M, one uncached 960k prompt is $4.80, per turn.
Don't be confused. The
inputcolumn being ~2 tokens does not mean the model read nothing; it means everything else arrived through the cache columns. Billed prompt =input + cache_creation + cache_read, always. What the columns change is the price per token (1x, 2x, 0.1x), not what the model attends to. A 97.5% hit rate session still processes every token of every prompt.
The price of walking away
Five of the six invalidations are TTL lapses: gaps of 69 minutes to 23 hours where the 1-hour cache expired and the next call had to re-write the whole conversation. Summed from the table: 2,064,286 tokens re-written at 2x, about $20.64, where the same tokens read from a warm cache would have cost $1.03. Call 157 alone, returning after a 527-minute break to a 751k-token conversation, cost $7.51 to re-warm.
That is roughly 18% of this session's total bill spent on five moments of stepping away, and
it prices a habit: returning to a huge stale session is a purchase. The alternatives all
beat it. Finish the thought before leaving; or come back inside the hour; or, if the return
is tomorrow, /clear and reopen with a summary (a fresh 20k-token start re-warms for
pennies) rather than re-warming a 751k relic; or let the session end and start the next one
from memory instead of transcript. The autopsy turns that advice from
folklore into a line item.
One more honest observation: through every lapse, the read column never drops to zero; a ~16k-token base stays warm. The account's other traffic (parallel sessions, subagents) was keeping the spine alive, which is the radix tree from Chapter 43 visible in a production bill: the trunk practically never expires because someone is always touching it.
The proof sessions
The autopsy is one session; these recipes let you regenerate every claim at will. Each is a
small experiment: run it, then point cache_autopsy.py at the session file it created (its
path prints in claude -p --output-format json as session_id; the file appears under
~/.claude/projects/<project>/). Commands are current as of July 2026; treat the outputs as
shapes to expect, not numbers to match.
Proof 1: the cache exists (two calls, one minute).
sid=$(uuidgen)
claude -p --session-id "$sid" "Reply with exactly: OK"
claude -p --resume "$sid" "Again, reply with exactly: OK"
python3 cache_autopsy.py ~/.claude/projects/<project>/"$sid".jsonl
Expected shape: call 0 writes the prompt (write large, read maybe nonzero if the spine is
warm), call 1 reads what call 0 wrote and writes only the tiny new turn. If you also compare
the two total_cost_usd values from --output-format json, the second call is visibly
cheaper per prompt token.
Proof 2: the TTL is real (the same experiment, plus a lunch break). Run proof 1, wait
past the TTL (over an hour for a Claude Code session; over five minutes for a default API
integration), then send a third turn with --resume. The autopsy shows the third call as an
INVALIDATION: TTL lapsed line: the read column collapses, the write column re-buys the
conversation. You have now measured the walking-away tax on a toy, which is exactly what call
157 above is at scale.
Proof 3: editing the prefix invalidates everything below it. In a working session, note
the current turn's read tokens (statusline, /context, or the transcript), then edit
CLAUDE.md (add one comment line) and send another turn. The autopsy shows the call after
the edit as INVALIDATION: prefix changed upstream: read crashes to the spine, write re-buys
the conversation. The same experiment with an MCP server toggle (claude mcp add / remove
between turns) shows the same signature higher up: tool schemas sit above CLAUDE.md in the
prompt, so the crash is deeper. This is why Chapter 40 charges
churn against always-loaded files: you just watched the charge.
Proof 4: /clear and /compact have opposite cache signatures. In a long session, run
/compact: the next call writes a medium prompt (the summary replaced the history; the old
cache is useless, the new one is small). In a second long session, run /clear: the next
call is nearly a cold start at spine size. Both show as "context shrank" invalidations in the
autopsy; the difference is the size of what gets re-written afterward, and
Chapter 42's Recipe 3 adds the other axis: what the summary
kept.
Proof 5: the fleet shares the trunk. Start a fresh session and immediately check call 0's
read column: on a machine with recent Claude Code activity it is already thousands of tokens
(this autopsy's was 15,853). Then launch a subagent (any Task-style delegation) and autopsy
its transcript: its first call also reads the spine it never paid to write. Sharing across
requests is not a serving-engine exotic; it is on your bill.
Remember. The order of evidence is transcript first, gauges second, vendor claims last. Anything this book says about caching, and anything a tool README says about saving tokens, should be reproducible as a shape in
cache_autopsy.py's output on your own machine within ten minutes. If it is not, the claim does not apply to your workload, no matter whose benchmark says otherwise (Chapter 27).
Further reading
- Chapter 24: the constructed-prompt version of these rules (breakpoints, minimums, the TTL decision) with the invalidator audit this autopsy caught in production.
- Chapter 25 and Chapter 23: the transcript format, the usage block field by field, and the whole-machine ledger this script is the microscope version of.
- Anthropic prompt caching docs
(
platform.claude.com/docs/en/build-with-claude/prompt-caching): the authoritative TTL, pricing multiplier, and invalidation-hierarchy reference the autopsy's arithmetic uses. - ccusage (Chapter 26): the productized ledger; its per-session view pairs well with this script's per-call view.
Takeaways
- Your transcripts are a complete, per-call cache record; the autopsy needs 130 lines of stdlib and no instrumentation you did not already have.
- One real two-day session: 97.5% hit rate, 6.2x cost reduction, 1-hour TTL on every write, peak prompt 960k tokens. Caching is not an optimization at that depth; it is the precondition.
- Six invalidations in 272 calls, five of them TTL lapses that re-bought 2.06M tokens for
~$19.61 versus $1.03 warm: walking away from a huge session is a purchase, and
/clearplus a summary is usually the cheaper return. - Every cache rule has a ten-minute proof session: two-call existence, TTL lapse, prefix-edit
invalidation,
/clearversus/compactsignatures, and the shared trunk in call 0's read column. - Evidence order: transcript, gauges, vendor claims. Reproduce the shape locally or treat the claim as unproven for your workload.
👉 The autopsy showed your fleet's tree from the bill side, and the lineage chapter showed who invented each layer of it. What remains is to compress everything this book has measured into the card you keep next to the keyboard: every lever, its rule, and the experiment that proves it. Continue to The practice card.