Back to blog
Trends

Anthropic Finally Tells You Why Your Cache Missed: How the New Diagnostics Cut My Wasted Tokens 30% in 7 Days

ยท11 min read
Anthropic Finally Tells You Why Your Cache Missed: How the New Diagnostics Cut My Wasted Tokens 30% in 7 Days

On May 6, 2026, Anthropic shipped cache diagnostics in public beta at Code with Claude [1]. Add one beta header to your Messages requests and the API now tells you, on every response, exactly why your prompt cache missed: the model changed, the system prompt drifted, the tools were reordered, or the message history was rewritten instead of appended.

Before this, the only signal you had was usage.cache_read_input_tokens dropping to zero. You knew the cache missed, you did not know why. So you bisected. Was it the timestamp you bake into the system block for telemetry? A reordered Pydantic schema? An assistant turn re-serialized between requests? Each hypothesis cost an hour of replay traffic to confirm or rule out.

I ran the new beta for 7 days against a workload that S8 had already flagged as silently wasting 17 to 26% of its API budget. The result: three regressions I had been paying for since March, one fixed in 11 minutes, two in under an afternoon each. Cache-read tokens went from a daily average of 38% of the cacheable prefix back to 92%. Bill on that workload dropped about 30%.

This guide walks through what diagnostics actually returns, the six cache_miss_reason types translated into fixes a one-person team can apply, and a decision matrix for when the beta is worth the extra HTTP header.

Anthropic cache diagnostics featured
Anthropic cache diagnostics featured

The Problem Before May 6

S8 documented the original symptom in March 2026: API bills creeping up 17 to 26% with no visible code change after Anthropic quietly cut the prompt cache TTL from 1 hour to 5 minutes. I went through that audit on my own workloads in late April and patched the obvious leak (1-hour TTL flag, heartbeats around long agent loops). Two workloads still drifted.

The diagnostic signal was a single counter: usage.cache_read_input_tokens. When it went to zero on a turn where the prefix should have been cached, your cache missed. There was no second counter. No reason field. No diff. You owned the bisect.

My fastest bisect averaged 4 hours. I would log every field of every request that flowed through the suspect endpoint for a day, then diff turns where reads dropped against the previous turn. Some divergences were obvious in five minutes (a stringified UUID I had forgotten about). Others required walking the tool schema serialization order across two Python versions before finding that one of them did not sort keys.

Cache diagnostics ships the answer with each response. The bisect goes from hours to one HTTP header round trip.


The API in 60 Seconds

The beta header is cache-diagnosis-2026-04-07. Add it to every request. On turn 1, opt in by passing "diagnostics": {"previous_message_id": null}. On turn N+1, pass the id from turn N's response.

import anthropic

client = anthropic.Anthropic()

# Turn 1: opt in, no prior message to compare yet
r1 = client.beta.messages.create(
    model="claude-opus-4-7",
    max_tokens=1024,
    cache_control={"type": "ephemeral"},
    system=SYSTEM_PROMPT,
    messages=[{"role": "user", "content": "Summarize section 1."}],
    diagnostics={"previous_message_id": None},
    betas=["cache-diagnosis-2026-04-07"],
)

# Turn 2: thread the previous id forward
r2 = client.beta.messages.create(
    model="claude-opus-4-7",
    max_tokens=1024,
    cache_control={"type": "ephemeral"},
    system=SYSTEM_PROMPT,
    messages=[
        {"role": "user", "content": "Summarize section 1."},
        {"role": "assistant", "content": r1.content},
        {"role": "user", "content": "Now summarize section 2."},
    ],
    diagnostics={"previous_message_id": r1.id},
    betas=["cache-diagnosis-2026-04-07"],
)

Want to know how effective your prompts are? Prompt Score analyzes them on 6 criteria.

Try it free

if r2.diagnostics and r2.diagnostics.cache_miss_reason: reason = r2.diagnostics.cache_miss_reason print(f"miss: {reason.type}, ~{reason.cache_missed_input_tokens} tokens")


The `diagnostics` field on the response has four possible states [2]:

- **Field absent**: you forgot the beta header.
- **`null`**: turn 1 (nothing to compare), or comparison ran and prefixes match.
- **`{"cache_miss_reason": null}`**: comparison still running when the response was serialized. Inconclusive, check next turn.
- **`{"cache_miss_reason": {...}}`**: divergence detected, with `type` and an estimate in `cache_missed_input_tokens`.

For streaming, the `diagnostics` field rides on the `message_start` event. Anthropic publishes working examples in nine languages including TypeScript, Go, Java, C#, and Ruby. Cache diagnostics is currently Claude API only: not on Bedrock, not on Vertex.

The fingerprints Anthropic stores are hashes and token-count estimates only, scoped to your organization and workspace. ZDR-eligible. Safe to enable in regulated environments [2].

---

## The Six `cache_miss_reason` Types, in Fixes

`cache_miss_reason` is a discriminated union on `type`. Anthropic reports only the earliest divergence, so fixing it may unmask a later one. Iterate.

### 1. `model_changed`

Cause: the `model` field differs from the previous turn. Usually a router, an A/B test, or a fallback selected a different model. The prompt cache is per-model, so any switch invalidates it.

Fix: hold the model constant within a cached conversation. If you genuinely need a fallback (rate limits, outages), accept that the fallback path will not cache and route around it explicitly.

### 2. `system_changed`

Cause: your `system` parameter is not byte-stable. Classic offenders:

- `f"Current time: {datetime.utcnow()}"` baked into the system block for "telemetry."
- Per-request UUIDs interpolated for trace correlation.
- A version string that gets bumped on each deploy and is read at request build time.

Fix: make the system prompt a compiled artifact. Move dynamic data into the first `user` message after your cache breakpoint. The cache prefix wants a frozen string, not a template.

### 3. `tools_changed`

Cause: the `tools` array differs. Either tools were added, removed, or reordered, or the `input_schema` JSON was serialized non-deterministically.

Fix: lock the tool list in a fixed order. If you build schemas from Pydantic, FastAPI, or zod, force key ordering: `json.dumps(schema, sort_keys=True)` in Python, equivalent helpers elsewhere. One of my regressions was a Pydantic schema whose order depended on import order between two services. The schemas were semantically identical and the cache treated them as different prompts.

### 4. `messages_changed`

Cause: an earlier entry in `messages` was altered, reordered, or removed instead of appended to. Two common patterns:

- You rebuild the `assistant` content from a database every turn instead of echoing back the original blocks Claude returned.
- Tool result blocks get re-serialized differently on resend (whitespace, key order).

Fix: treat history as append-only. Echo back `assistant` content verbatim, store it as Claude returned it, do not regenerate. Same for `tool_result` blocks.

### 5. `previous_message_not_found`

Cause: no stored fingerprint exists for the supplied `previous_message_id`. This is NOT evidence that your request changed.

Three sub-cases:

- The previous request did not carry the beta header.
- It was issued from a different workspace or org.
- Too much time has passed (fingerprints expire after a short window).

Fix: send the beta header on every turn. Keep consecutive turns close in wall-clock time. If you batch nightly, the cache is gone anyway and diagnostics will not help.

The techniques you're reading about work. Test your prompts now with Prompt Score and see your score in real time.

Test your prompts

6. unavailable

Cause: model, system, and tools match, but another prompt-affecting request parameter differs. The candidates: tool_choice, thinking, context_management, output_config, output_format, or the set of active anthropic-beta headers. Or the conversation is so long the divergence is beyond Anthropic's comparison horizon.

Fix: keep prompt-affecting parameters constant for the lifetime of a cached conversation. If unavailable is persistent and you have audited the listed parameters, fall back to the manual checks Anthropic documents under the prompt-caching troubleshooting guide.

All four *_changed types also carry cache_missed_input_tokens, an estimate of how much cacheable prefix fell after the divergence. Anthropic warns this is a magnitude indicator derived from byte lengths, not a billing number, but it lets you prioritize which fix unblocks the most spend.

Six cache_miss_reason types and their fixes
Six cache_miss_reason types and their fixes

My 7-Day Audit

I ran the beta against two workloads still flagged after the S8 patch pass: a multi-turn document analyzer and an agent loop that takes 8 to 15 minutes between human inputs.

Day 1-2. Instrumentation rollout. I gated the beta header behind a feature flag covering 8% of traffic. Zero downside risk: diagnostics never fails or blocks the request. Worst case is the field is null or unavailable.

Day 3. First reason emerged. system_changed on 71% of cache-miss turns. Root cause: a "telemetry_request_id": <uuid> field I had added to the system block in March specifically to correlate request logs. I moved it out into the first user message after the cache breakpoint. 11 minutes including the deploy. Cache reads on that workload climbed back to roughly their pre-March baseline within an hour as old fingerprints expired and new ones formed.

Day 4-5. tools_changed on the document analyzer. Two services were posting to the same Anthropic endpoint with schemas built from the same Pydantic models, but Python's dict insertion order differed between them because of a refactor that had pinned one service to an older pydantic version. The schemas serialized to JSON with keys in different orders. I forced sort_keys=True on both. About 14% of the remaining cache misses on that workload disappeared.

Day 6. messages_changed on the agent loop. I had been rebuilding the assistant content blocks from a Postgres column instead of echoing back what Claude returned. The text content was identical, but I was reconstructing structured blocks from a database row and losing the original block ordering. Switched to append-only and the final 9% of misses cleared.

Day 7. Diff vs day 1 baseline on the two workloads:

MetricBeforeAfter
Average cache hit rate per turn38%92%
cache_creation_input_tokens per day4.1M0.7M
Estimated monthly spend on that workload$610$420

About 30% bill reduction on the workloads I instrumented. None of the three fixes was a redesign. All three were latent bugs that my old diagnostic loop could not point at.


Decision Matrix: When to Turn This On

This is a beta. Field names and semantics can still change. It costs you one extra header on every request and a small amount of code to thread previous_message_id through your conversation loop. Not every workload is worth the install.

Use cache diagnostics if...Skip it if...
Cache-related spend is more than 10% of your monthly API billYou do not use prompt caching at all
Conversations or agent loops are 3+ turnsYou only run one-shot completions
You use tools, structured system prompts, or templated promptsYour prompts are simple user-only text
You ship to Claude API directlyYou route through Bedrock or Vertex (not supported)
You have at least one workload showing unexplained cache_read_input_tokens dropsCache reads are already 90%+ and stable

If three or more of the left column apply to you, the install is an afternoon and you keep the diagnostic on permanently. If two or fewer, it is probably not worth the code path until the feature exits beta.

Decision matrix when to enable cache diagnostics
Decision matrix when to enable cache diagnostics

What Changes for Small Teams

Three takeaways from running this for a week.

The diagnostic ships the answer instead of the symptom. Bisecting prompt-cache regressions used to take hours of replay traffic. It is now one header and a previous_message_id. The bisect cost on a regression dropped from 4 hours to under 10 minutes for me. If you have ever paid 20% more for nothing because a UUID slipped into the wrong block, this is the install that ends the class of bug.

Production prompts should be byte-stable by design, not by accident. The system prompt and tool schema are not free-form: they are compiled artifacts your cache treats as keys. Treat them that way. Sort schema keys. Pin Pydantic versions across services that share endpoints. Move all "freshness" into the first user message.

The cost of the install is one HTTP header. Cache diagnostics is ZDR-eligible: hashes and token counts only, no raw prompt content stored. Safe in regulated environments. No retry logic to maintain. If the comparison is unavailable or pending, your request still completes normally.

If you are a one-person team or a 5-person crew and your Anthropic bill is a meaningful line item, this is a 1-hour install that returned 30% on my workload. I would not have caught two of the three regressions without it.


Sources

[1] Anthropic Release Notes, Claude Platform, May 2026. Cache diagnostics public beta announcement at Code with Claude, May 6, 2026.

[2] Anthropic Docs, Cache diagnostics (beta), platform.claude.com/docs/en/build-with-claude/cache-diagnostics. Beta header, request schema, response shape, cache_miss_reason types, and ZDR retention policy.

[3] Anthropic Docs, Prompt caching, platform.claude.com/docs/en/build-with-claude/prompt-caching. Cache write/read pricing, TTL options, troubleshooting guide.


Keep Reading

Want to know which of your prompts are wasting tokens before the API tells you? Score them with Keep My Prompts and see the 6 quality criteria your team's prompts already pass or fail.

#anthropic#claude#cache-diagnostics#prompt-caching#api-cost#beta#debugging#observability#indie-dev#small-team#2026

Ready to organize your prompts?

Start free, no credit card required.

Start Free

No credit card required

Related articles