[BUG] Prompt Cache: Full Conversation Prefix Re-written 3 times Seconds Apart (Multiple Sessions & Models)

Status Open
Maintainer reply None cached
Activity 4 comments · opened Jul 30, 2026

Prompt cache: full conversation prefix re-written on turns seconds apart — 31% of 3 weeks of cache-write spend, recurring across 4 model families

Summary

Claude Code intermittently re-writes the entire conversation prefix to the prompt cache on a
turn that should have been a near-total cache hit. cache_read_input_tokens collapses to just
the static system-prompt + tools segment (~20–24k) while cache_creation_input_tokens jumps to
the full context size (observed up to 899,075 tokens in a single request).

I scanned 3 weeks of my own local transcripts: 732 sessions, 13,688 API requests, 83,666,423
total cache_creation_input_tokens.

  • 85 full-prefix rewrite events across 27 sessions and 5 projects
  • Those 85 events = 26,211,061 cache-write tokens = 31.3% of ALL cache-write in 3 weeks
  • 25 of them happened with less than 300s of idle time before the request — inside even the

shortest cache TTL. That subset alone is 7,454,315 cache-write tokens ≈ 9.3M
input-token-equivalents
(writes bill at 1.25×)

  • 11 events with under 60s idle; the tightest is 11 seconds
  • 9 events read back 0 tokens — even the static system prefix missed
  • Occurs on **claude-opus-4-8 (30), claude-opus-5 (42), claude-fable-5 (11),

claude-sonnet-5 (2)** — not model-specific

  • Also occurs in subagent and workflow transcripts, not just main sessions

The user-visible symptom is a session budget vanishing with nothing to show for it. In the
incident that prompted this report, 743,839 cache-write tokens were billed in 70 seconds while
total output for the window was 17,334 tokens
— roughly 30% of a 5-hour session limit consumed
by cache writes, not generation.

I am reporting two things: (A) the rewrite-on-consecutive-turns behaviour, and (B) a
session-usage accounting question at the limit-window reset boundary.

Environment

  • Claude Code CLI, macOS (Darwin 25.5.0), zsh
  • Models affected: claude-opus-4-8, claude-opus-5, claude-opus-5[1m], claude-fable-5,

claude-sonnet-5

  • Contexts involved: 100k–900k tokens
  • Hooks active in the affected sessions: a UserPromptSubmit hook (per-prompt context

injection), a PreCompact hook, SessionStart hook output

  • Heavy Edit/Bash tool loops; some sessions include image attachments and subagent/workflow runs
  • The primary incident session declared: *"This session's requests use a 1-hour Anthropic

prompt-cache TTL", with the caveat "If the session enters usage overage, later requests drop
to the 5-minute TTL"*. The account was in usage overage during that incident.

Times below are Europe/Berlin (CEST, UTC+2).

(A) Full-prefix rewrite on a turn seconds after the previous one

Clearest single repro — two rewrites 41 seconds apart

Same session, claude-fable-5, consecutive requests:

13:35:14  cacheWrite=370497  cacheRead=24119   req_011CdYAso2C4HxMbkLM95ZgW
13:35:55  cacheWrite=373342  cacheRead=24119   req_011CdYAxUfH9AjBEotAUfKxw   <- 41s later, missed again
13:36:24  cacheWrite=  1804  cacheRead=397461  req_011CdYB1Fycqjboh1pgUStaQ   <- recovered

The 370,497 tokens written at 13:35:14 were billed and never read by any request. The turn
41 seconds later re-wrote the same prefix from scratch. The turn after that hit correctly,
reading 397,461. No TTL explains a miss at 41s, and the recovery one turn later shows the data
was cacheable the whole time.

4 of the 85 events are immediately followed by another full rewrite like this.

Cross-session evidence: rewrites with <300s idle before them

Every one of these is inside even the 5-minute overage TTL, so eviction cannot be the cause:

date/time (Berlin)  model             idle    cacheWrite   cacheRead
2026-07-28 10:20    claude-opus-5       82s      899075       20601
2026-07-23 17:22    claude-opus-4-8    245s      411677       20532
2026-07-27 17:47    claude-opus-5       70s      398768       20601
2026-07-24 18:49    claude-fable-5      17s      389753       24305
2026-07-28 11:17    claude-opus-5       50s      378091       20601
2026-07-30 13:35    claude-fable-5      41s      373342       24119
2026-07-24 18:39    claude-fable-5      14s      359981       24305
2026-07-24 21:05    claude-opus-5      208s      346415       24105
2026-07-30 12:37    claude-fable-5      99s      340873       24119
2026-07-27 16:49    claude-opus-5       82s      318111       20601
2026-07-25 07:48    claude-opus-5       48s      300892       21997
2026-07-30 12:13    claude-fable-5      38s      299100       24119
2026-07-16 17:46    claude-opus-4-8    152s      253730       23401
2026-07-27 13:29    claude-sonnet-5    271s      247018       28923
2026-07-28 17:31    claude-opus-5      179s      232208       24109
2026-07-28 17:25    claude-opus-5       41s      219377       24109
2026-07-24 17:42    claude-fable-5      52s      211015       24305
2026-07-17 18:14    claude-opus-4-8     11s      208908       56930
2026-07-28 14:34    claude-opus-5      104s      203921       24109
2026-07-30 11:37    claude-fable-5     158s      199215       24119
2026-07-13 11:24    claude-opus-4-8     82s      187750       21749
2026-07-13 12:01    claude-opus-4-8     23s      185656           0   (workflow subagent)
2026-07-16 17:58    claude-opus-4-8    104s      181065       23722
2026-07-28 17:07    claude-opus-5       49s      120624           0   (subagent)

(One row appears twice in my scan because a resumed session logs the same requestId in two
transcript files — 24 distinct events, 25 records.)

The escalation pattern

Within a single session, each successive rewrite costs more, because the prefix being re-written
keeps growing. One session, 558 requests:

2026-07-27 16:30  cacheWrite=240995  cacheRead=20601
2026-07-27 16:49  cacheWrite=318111  cacheRead=20601
2026-07-27 17:47  cacheWrite=398768  cacheRead=20601
2026-07-27 21:15  cacheWrite=562592  cacheRead=20601
2026-07-27 22:13  cacheWrite=677381  cacheRead=24108
2026-07-28 08:00  cacheWrite=740887  cacheRead=20601
2026-07-28 09:54  cacheWrite=784944  cacheRead=20601
2026-07-28 10:20  cacheWrite=899075  cacheRead=20601   <- 82s idle before this one

8 events = 4,622,753 tokens = 83% of that session's entire cache-write spend. The last one
re-wrote 899k tokens after 82 seconds of idle.

cache_read_input_tokens is pinned at exactly the same value (20601 / 24119 / 24305 / 21997,
depending on session) across every rewrite in a session, over many hours. Only the first cache
breakpoint — static system prompt + tool schemas — ever hits on these turns. The
conversation-body breakpoint is written and never read.

Expected vs actual

Expected: a request issued seconds after the previous one, same model, same session, prefix
unchanged apart from appended turns, reports cache_read_input_tokens ≈ N and
cache_creation_input_tokens ≈ delta.

Actual: cache_read_input_tokens drops to the static-prefix size and the full body is
re-written. Sometimes twice in a row before it recovers on its own.

Idle-gap distribution for all 85 events

<60s        11  (13%)
60-300s     14  (17%)
300-3600s   21  (25%)
>3600s      37  (45%)
median 1102s

The 37 events after >1h idle are legitimate expiry and I am not claiming those are bugs — but
see the visibility request below, because a legitimate 900k-token rewrite is still a large
silent charge. The 25 events under 300s are the defect.

(B) Session-usage accounting at the limit-window reset

Timeline for the incident that prompted this, on 2026-07-30:

| time (Berlin) | event |
|---|---|
| ~13:23 | hit 100% of the 5-hour session limit; continued on usage credits (overage) |
| ~13:29 | Fable 5 (effort: high) actively working a task |
| 13:30 | session limit window resets |
| 13:31 | /usage still reported 100% session usage, one minute after the reset |
| ~13:33 | /usage reported ~30% of the new window already consumed |
| 13:35:14 | full prefix rewrite, 370,497 tokens |
| 13:35:55 | full prefix rewrite, 373,342 tokens |
| 13:36:31 | user interrupts all work |
| 13:36:45 | /compact |
| 13:40:45 | switch to Opus 5 to investigate |

Transcript-verified: the interrupt, compact, and model switch match to the second.

Two observations:

  1. At 13:31, one minute after the reset, /usage still showed 100%. The displayed window

boundary and the billing window boundary may not be on the same clock.

  1. The ~13:33 reading of ~30% consumed predates any billed request in the new window. The

transcript shows no API request at all between 13:27:15 and 13:35:14. Either pre-reset
spend was attributed to the post-reset window, or my ~13:33 recollection is off by ~2 minutes
and the reading actually came after 13:35:55 — in which case (A) fully accounts for it and (B)
is not a bug. I cannot settle this from the transcript; flagging it in case the reset-boundary
accounting is worth a look on your side.

Requests

  1. Fix the sub-300s rewrites. A request seconds after a successful cache write should not

re-write the whole prefix. The self-recovery one turn later suggests a write-visibility or
breakpoint-identity problem rather than expiry.

  1. Verify the declared TTL is actually applied to the conversation-body breakpoint, not only

the system prefix. Sessions advertising a 1-hour TTL show body misses after 8 minutes.

  1. Surface cache_creation_input_tokens spikes in /usage or the status line. Today a

370k–899k prefix rewrite is completely invisible; it looks identical to normal work while
consuming a third of a session budget. This is the single change that would have saved me from
burning two session windows without knowing why.

  1. Check cache-breakpoint stability when hooks inject content mid-conversation. In the

13:35 incident, hook output landed at 13:35:31 and 13:35:41 — between the two failing
requests. A UserPromptSubmit hook appending per-prompt text, or file-state
system-reminders, could be shifting content behind a breakpoint and invalidating everything
after it.

  1. Reconsider shortening the cache TTL on overage (or warn clearly). Overage is exactly when

a miss is most expensive, and the shortened TTL makes misses more frequent — a feedback loop
that accelerates spend precisely when the user is already paying premium.

  1. Consider crediting back demonstrable sub-TTL rewrites. 9.3M input-token-equivalents over

three weeks from the <300s subset alone is not a rounding error.

Note for anyone reproducing this analysis

Transcripts emit one assistant record per content block, each carrying a copy of the same
usage object. My primary session logged 471 assistant records for 220 real requests. Dedupe by
requestId or you will over-count by ~2×.

Detection predicate used: cache_creation_input_tokens > 100_000 AND cache_read_input_tokens <
0.3 * cache_creation_input_tokens
, deduped by requestId, with idle gap measured to the
previous request in the same transcript.

Local transcripts with full per-record usage (468 MB across 732 sessions) are retained and can
be supplied on request, including the two requestIds named above.

View original on GitHub ↗

3 Comments

Regsorm · 2 months ago

Confirming this on Windows with a larger sample — same pattern, higher share of cache-write spend.

Environment: Claude Code v2.1.220, Windows, Max 20x. Models: claude-opus-5, claude-opus-4-8.

I ran your detection over my own transcripts (10 days, 2026-07-20 onward), deduplicated by message.id:

  • 396 sessions, 16,726 requests, 129,495,251 total cache_creation_input_tokens
  • 258 full-prefix rewrite events (cache_write > 100k while cache_read collapsed)
  • Those events = 72,124,190 cache-write tokens = 55.7% of ALL cache-write over the period (your figure: 31.3%)
  • 110 events with under 300s idle before the request — inside any TTL — totalling 26,367,609 cache-write tokens
  • Shortest gaps observed: 21s, 28s, 35s, 49s
  • One event read back 0 tokens (2026-07-27 16:19:28, cache_write 715,426, cache_read 0)

Clearest short-idle examples:

| Time (local) | Model | cache_write | cache_read | idle |
|---|---|---|---|---|
| 07-30 09:21:28 | claude-opus-5 | 532,933 | 29,796 | 21s |
| 07-23 13:23:54 | claude-opus-4-8 | 541,154 | 29,914 | 28s |
| 07-24 16:47:18 | claude-opus-4-8 | 598,352 | 38,571 | 35s |
| 07-24 06:25:05 | claude-opus-4-8 | 806,011 | 29,930 | 49s |
| 07-30 09:14:47 | claude-opus-5 | 519,030 | 25,690 | 177s |
| 07-28 00:15:41 | claude-opus-5 | 529,866 | 28,748 | 199s |

cache_read pins at 25k–38k across these — matching your observation that only the static system-prompt + tools breakpoint ever hits, while the conversation body is written and never read. Split across claude-opus-4-8 and claude-opus-5 alike, consistent with your finding that this is not model-specific.

Two caveats on my numbers:

  • My "read collapsed" threshold was ≤70k rather than your tighter ~20–24k static-prefix size, so 258 is an upper-bound count. The short-idle subset above is unaffected by that threshold choice — those reads all landed at 25k–38k.
  • "Idle" is measured as the gap between consecutive usage-bearing records within the same session file, which approximates the inter-request gap rather than measuring it directly.

Separately I had been tracking this from the usage-meter side in #81234 and #82113 — my workload is ~90% cache-read by token count and the weekly meter moves far faster than output volume would suggest. This report gives a concrete mechanism that fits that far better than the metering theories I was working from.

cunitacodeitor · 2 months ago

Angel here, founder of Runa. Your 85-event analysis puts a hard number on the waste: 31.3% of all cache writes, with 25 misses inside even the shortest TTL. The Windows replication at 55.7% makes this hard to dismiss as one environment.

I'm building https://runacode.io around the cost and visibility side of this problem: run Claude Code on a cloud machine, and Run Inspector shows where the tokens went and what was saved. We've measured about 46% lower token cost on real runs. New users get $50 of machine usage (you bring your own model API key).

If you're open to trying it on one real task from your repo, I'll pair with you for 30 minutes and compare the run in Inspector. If it doesn't cut tokens without hurting the work, we call it a miss. Worth a try?

edubraqd · 29 days ago

Replication with the same method on a different machine, plus a version-boundary observation that may help triage.

641 local sessions, 133,824 calls, Claude Code 2.1.156 → 2.1.272, Windows, Claude subscription (1h TTL), first-party API, usage deduplicated by message.id. A "break" = a call whose cache_creation exceeds 20% of the previous context and 20k tokens.

| cause | events | tokens written | share of all cache writes |
|---|---:|---:|---:|
| TTL (gap > 60 min) | 745 | 306M | 26% |
| resume / model switch | 161 | 66M | 6% |
| full re-write with no gap (cache_read = shared prefix only, context did not shrink) | 1,336 | 570M | 48% |
| all breaks | 2,258 | 949M | 80% |

Your 31% of write spend in 85 events is the third row; here it is 48% in 1,336 events, median re-write 431k tokens. Over all 132,016 no-gap boundaries (same model, no shrink) it happens 0.9% of the time: 1.4% after a user turn vs 0.2% after a tool call — the new user turn is where the client re-renders the prefix.

What the transcript holds between the two calls, and the re-write rate when it is present: effort change 30.8% (169 events), tool-list delta 10.6%, MCP reconnect 10.2%, skills delta 9.5%, UserPromptSubmit hook additionalContext 8.3% (5,877), baseline 0.9%. Those explain ~160 events / 65M tokens. The rest is concentrated by client version, not by anything the user did:

| Claude Code version | boundaries | re-wrote | rate |
|---|---:|---:|---:|
| 2.1.215 | 1,288 | 419 | 32.5% |
| 2.1.181 / 2.1.202 / 2.1.209 | 15,763 | 283 | 1.8% |
| 2.1.219 / 2.1.221 / 2.1.205 | 18,335 | 156 | 0.9% |
| 2.1.229 / 2.1.222 | 19,060 | 94 | 0.5% |
| 2.1.237 and later | 60,000+ | 71 | 0.1–0.2% |

One 2.1.215 session (Sonnet 5, 992 calls, 16 h) holds 417 of them: from ~566k context to the end nearly every call re-wrote ~520k with only the 48k shared prefix cached — 216M tokens in one session, and the only transcript line between the last cached call and the first re-written one is a custom-title update. Your report (2026-07-30, Opus 4.8 / Opus 5 / Fable 5) predates 2.1.237, i.e. it sits in the same client range. Whatever it was, it is mostly gone after 2.1.237: 36 unexplained events in 60k+ boundaries, 11.9M tokens. The rate still rises with context size (0.4% under 200k → 1.9% at 500–800k).

Scripts (tools/breaks.py, tools/rewrites.py, tools/ledger.py) and the write-up: https://github.com/edubraqd/claude-context-forensics, report/findings.md §4.

Showing cached comments. Read the full discussion on GitHub ↗