CostCompassAn AlmanacBeta
Engineering Guide

A stuck cache is a diagnosis, not a mystery

Prompt caching breaks quietly. The request succeeds, no error is raised, and the bill just doesn't move. But the provider already told you what happened in the response. This guide reads those counters as a decision tree: did the cache write, did it read, and did it save, and if the read failed, exactly which part of your request broke the match.

By Joubert BergerUpdated October 8, 2026

You did everything the how-to said. You structured a stable prefix, added the cache marker, and shipped it. The requests still succeed, no error shows up in the logs, and the bill looks the same as last week. Nothing is broken in any way your monitoring can see, and that is the problem. Prompt caching doesn’t fail loudly. When it fails, the response comes back normal and the only trace is a number in the usage object that you were not looking at.

If caching is on and not helping, start here. For the economics, see Prompt Caching Explained; for setup, see How to Enable Prompt Caching.

Every provider puts enough in the response to find the failure: read two counters in order, and if the second is wrong, narrow it to one part of the request.

The one question that becomes three

When caching isn’t working, the provider’s own token counters tell you which of three things failed, and you check them in a fixed order:

  1. Did the cache write? Was anything stored at all?
  2. Did the cache read? Was the stored prefix matched on a later request?
  3. Did it actually save? Did the reads outweigh what the writes cost?

Each question has its own counter and its own fixes. A write-side failure means nothing was stored: the write counter is zero. A read-side failure means something was stored but never matched; the write counter is non-zero and the read counter is zero. An economics-side failure means the cache stored and read as designed, but the reuse was too thin to pay back the write premium. Both counters look healthy and the bill still didn’t drop.

The order matters because the questions are dependent. There is no point hunting for a broken prefix if nothing was ever stored, and there is no point weighing savings if the cache never reads. Walk the tree top to bottom and each answer either resolves the problem or hands you to the next question.

A top-to-bottom decision tree for diagnosing a prompt cache. The first node reads the write counter; a zero write branches to the write-side region (nothing stored: below minimum size, or a marker, format, or gateway mistake). A non-zero write leads to the second node, which reads the read counter on a repeat request; a zero read branches to the read-side region, which fans out into four request segments — model, system prompt, tool definitions, and message history. A non-zero read leads to the third node, which asks whether reads outweigh writes; if not, it branches to the economics-side region (reuse below break-even, TTL expiry, sparse traffic).
Figure 1. The cache-failure decision tree. Read the write counter, then the read counter, then compare reads against writes. Each answer places the failure in one of three regions — write-side, read-side, or economics-side — and a read-side failure narrows further to the one request segment that broke the match. This is the whole diagnostic method.

The counters, and what they are called on your provider

You cannot run the decision tree without reading the counters, so start by finding them. Conceptually every provider reports the same three quantities on each response: how many input tokens it wrote to the cache, how many it read from the cache, and how many it processed fresh (the uncached remainder). The names differ, and the numbers are dated because they and the surrounding APIs drift, so reverify against the provider’s own docs before you rely on them. As of September 22, 2026:

ProviderCache-write counterCache-read counterUncached remainder
Anthropiccache_creation_input_tokenscache_read_input_tokensinput_tokens
OpenAI GPT-5.6+ (Responses API)cache_write_tokens (under usage.input_tokens_details)cached_tokens (same object)input_tokens less writes and reads
OpenAI earlier response shapes(no separate write counter)cached_tokens (under usage.prompt_tokens_details)prompt_tokens less cached
Amazon BedrockcacheWriteInputTokenscacheReadInputTokensinputTokens
Google Gemini(reported via cached-content count)cached-content tokens (under usageMetadata)remainder of prompt tokens

The field names come from each provider’s prompt-caching documentation: Anthropic, OpenAI, Amazon Bedrock, and Google Gemini.

OpenAI’s GPT-5.6 and later Responses API usage reports both cache writes and reads. Those models charge 1.25× the ordinary input rate for writes, whether the breakpoint was chosen implicitly or explicitly. Earlier OpenAI response shapes expose only cached reads. Where both counters exist, their split distinguishes “nothing was stored” from “something was stored but not matched.”

Here is what a write-then-read pair looks like on the wire. The counts below are illustrative, not from a live call. On Anthropic (Claude API), the write and read counters move in opposite directions between the two calls:

// Call 1 — writes the prefix (read counter still 0)
"usage": {
  "input_tokens": 18,
  "cache_creation_input_tokens": 1534,
  "cache_read_input_tokens": 0,
  "output_tokens": 92
}
// Call 2 — same prefix, now served from cache
"usage": {
  "input_tokens": 18,
  "cache_creation_input_tokens": 0,
  "cache_read_input_tokens": 1534,
  "output_tokens": 88
}

Amazon Bedrock reports the same split under camelCase names:

// Call 2 — the read
"usage": {
  "inputTokens": 18,
  "cacheWriteInputTokens": 0,
  "cacheReadInputTokens": 1534,
  "outputTokens": 87
}

On GPT-5.6 and later Responses API calls, compare cache_write_tokens on the first call with cached_tokens on the repeat call, both under usage.input_tokens_details. Earlier response shapes expose only cached_tokens; this example shows that older shape:

// Call 2 — prefix hit
"usage": {
  "prompt_tokens": 1552,
  "prompt_tokens_details": { "cached_tokens": 1536 }
}

Google Gemini breaks the cached share out of the prompt count under usageMetadata:

// Call 2 — cached content counted separately
"usageMetadata": {
  "promptTokenCount": 1552,
  "cachedContentTokenCount": 1536,
  "candidatesTokenCount": 88,
  "totalTokenCount": 1640
}

Over three calls, the healthy pattern is: the first call writes, the second and third read, their write counters dropping to zero and their read counters carrying the bulk of the input. If your traces look like that, caching is engaged, and any remaining problem is economics. If they don’t, the deviation tells you which region you are in.

Write-side: nothing was stored

Start here whenever the write counter is zero on the first call and the read counter stays zero on the repeat (or, on older OpenAI models, whenever cached_tokens stays zero even on the second identical call). A zero write means the provider never stored anything, so no amount of reuse will ever produce a read. Two causes account for nearly all of it.

The prefix is below the model’s minimum cacheable size. Every provider enforces a minimum: below some number of tokens in the cacheable region, it stores nothing, returns a perfectly normal response, and raises no error. The only symptom is that both counters stay at zero forever. The minimum is per-model, not just per-provider. As of October 8, 2026, Anthropic requires 512 tokens for Claude Opus 5.5, Sonnet 5.5, Haiku 5.5, Fable 5.1 and Mythos 5.1, and 1,024 for Claude Opus 4.8 and Sonnet 5, with different minimums for other models; OpenAI caches at 1,024 visible prefix tokens for GPT-5.6 and later, with older models varying by request settings; Amazon Bedrock sets it per model per cache checkpoint (1,024 tokens for Claude 3.7 Sonnet and Sonnet 4.5, 4,096 for Claude Opus 4.5); and Google Gemini ranges from 2,048 to 4,096 tokens depending on the model. Check the tokens ahead of your cache boundary against the minimum for the exact model you are calling, and confirm you are calling the model you think you are, because a fallback to a smaller model can raise the minimum without you noticing.

The marker never took effect. The prefix clears the minimum, but the cache instruction was malformed or lost, so the provider treated the request as uncacheable. The classic version on explicit providers is placing the cache control at the wrong level, attaching it to a plain-string system field at the message level, rather than to the content block the provider inspects (one report of exactly this). The request succeeds, the large system prompt is re-sent and billed in full on every call, and the counters never leave zero. The other common version is a gateway or SDK in the path that re-shapes your request and drops or relocates the marker in translation. A proxy, a framework adapter, or a provider-router layer that “supports caching” but only for a request shape yours doesn’t match. The tell is the same either way: your code enabled caching, and the counters say it didn’t.

Enlarge the prefix above the minimum, or place the marker where the provider expects it and confirm nothing between you and the provider is rewriting the request. The exact placement per provider belongs to the enablement guide.

A two-by-two grid mapping two counter readings to a failure region. Rows are the cache-write counter (zero or non-zero); columns are the cache-read counter on a repeat request (zero or non-zero). Write zero, read zero is write-side (nothing stored — below minimum size, or a marker, format, or gateway mistake). Write non-zero, read zero is read-side (stored but the prefix diverged; localize the segment). Write zero, read non-zero is normal (reading a prefix an earlier call wrote). Write non-zero, read non-zero with a flat bill is economics-side (verify reuse against break-even).
Figure 2. The two counters, read together, place the failure. A zero write is write-side; a non-zero write with a zero read is read-side; healthy counters with a flat bill are economics-side.

Once the write counter is non-zero, you know something was stored. If reads are still zero, the prefix is being stored and then missed.

Read-side: the cache writes but never reads

This is the failure that fills issue trackers — “cache read always 0” is its canonical form — because it looks contradictory: the write counter proves the cache is storing your prefix, yet the read counter is stuck at zero, so it is storing the same thing over and over and never reusing it. On a provider that charges a write premium that means you pay the write premium on every call and collect none of the read discount.

The mechanism behind every read-side failure is the same. The cache match is a byte-exact comparison of the leading prefix, evaluated in order from the first token, that stops at the first token that differs. Everything after that first difference is treated as uncached, even when it is identical to what you sent before. So a single per-request value that has drifted toward the front of the request resets the match for the entire prefix behind it.

A request has an ordered structure, and the divergence sits in one of four segments:

  • Model. The cache is per-model. If a router, an A/B split, or a fallback selected a different model than the request that wrote the entry, nothing matches, because the cache for one model is invisible to another. This is the cause behind a lot of “high writes, zero reads on most turns” reports (one, two, three): an auto-reply or retry path switches models.
  • System prompt. A per-request value interpolated into the system prompt is the single most common read-side cause. A timestamp, a request id, a session identifier, a personalized greeting, or a “current date” line near the top changes the leading bytes on every call and forces a fresh write each time.
  • Tool definitions. Tools added, removed, or reordered between turns change the prefix, as does a tool schema serialized with non-deterministic key order. Two runs that are semantically identical produce different bytes. Watch for layers that inject or reorder tools for you; some gateways place tool definitions ahead of the system prompt, so any tool change invalidates everything.
  • Message history. Caching a conversation assumes history is append-only. If an earlier message was edited, truncated, reordered, or re-serialized differently on resend — including assistant turns and tool results echoed back in a new shape — the prefix diverges at the point of the edit and everything after it misses.

There are two subtler variants worth naming because they hide inside those four. One is inconsistent serialization: a templating layer that normalizes whitespace differently between runs, or JSON emitted with unstable key order, so requests that look identical tokenize differently. The other is isolation boundaries: caches are typically scoped to an API key, workspace, or account, so two calls made with different keys or from different workspaces cannot reuse each other’s entries even when the bytes match perfectly.

A single request drawn as an ordered horizontal band of labelled segments — model, system prompt, tool definitions, message history, then a volatile tail. A vertical 'first divergence' marker sits partway along; everything to its left is shaded matched and cacheable, and everything to its right, including the rest of the prefix, is shaded treated as uncached. Each segment carries a short cause and fix: model — a router or fallback switched models; system — a timestamp or id interpolated in; tools — reordered or non-deterministically serialized; history — edited instead of appended.
Figure 3. The read-side segment map. The byte-exact match runs left to right and ends at the first divergence, so everything behind it — even identical content — is billed fresh. Localizing that first divergence to one segment (model, system, tools, or history) points straight at the cause and the fix.

The discipline follows directly: find the first divergence. Everything after it follows from it, so fix the earliest one and re-measure before chasing anything further back. The fastest way to find it is to let the provider do the comparison for you.

Let the provider point at the divergence

Eyeballing two large requests for a one-token difference is miserable and error-prone. On the Claude API, you don’t have to: the Cache Diagnostics feature compares two consecutive requests and tells you where they first diverged. It is a beta (header cache-diagnosis-2026-04-07, and its field names may change): you pass the previous response’s id as diagnostics.previous_message_id, and the response carries a cache_miss_reason naming the first segment that changed. It maps one-to-one onto the read-side segments above:

cache_miss_reason typeWhat divergedWhere to look
model_changedThe model differs from the prior requestA router, A/B test, or fallback picked a different model; hold the model constant
system_changedThe system prompt differsA timestamp, id, or per-request value was interpolated in; make the system prompt byte-stable and move dynamic data after the breakpoint
tools_changedThe tools array differsTools added, removed, reordered, or non-deterministically serialized; send a fixed list with sorted-key schemas
messages_changedAn earlier message was alteredHistory was edited, truncated, or re-serialized instead of appended; treat it as append-only
previous_message_not_foundNo stored fingerprint for that idNot evidence your request changed — the prior call likely lacked the beta header, used a different workspace, or was too long ago
unavailableNo comparison was producedAnother prompt-affecting parameter differs (for example tool_choice, thinking, or output settings), or the change is beyond the comparison horizon

Two cautions. The tool reports the earliest divergence only; later ones can be hidden behind it, so fix the reported segment and run it again. And it is Claude API only; it is not available on Amazon Bedrock or Google Cloud, and no equivalent cross-request diagnostic exists on the other providers. Where the tool isn’t available, you reproduce it by hand: serialize the exact bytes of two requests that should have matched and diff them front to back. You are looking for the first point where two supposedly-identical prefixes differ.

Cache Diagnostics also answers a question the raw read counter can’t. Its result combines with cache_read_input_tokens to separate “my request changed” from “my request matched but the entry was gone”:

Diagnostics resultCache read tokensInterpretation
No divergence found (null)highWorking as intended — stable prefix, cache hit.
No divergence found (null)low or zeroRequests matched, but the entry had already expired — a TTL problem, not a prefix bug; shorten the gap between calls or use a longer cache duration.
A *_changed reasonlow or zeroYour bug — the request changed; fix the named segment.
A *_changed reasonhighRare — a late change occurred but an earlier breakpoint still hit; low impact.

That second row is why the read counter alone isn’t enough. A zero read doesn’t always mean your prefix broke. The prefix may have been fine and the entry expired before you reused it. One case sends you to fix your request, the other to fix your timing.

Economics-side: it reads, and the bill still didn’t drop

Suppose you’ve done all of the above. The write counter is healthy, the read counter is non-zero and growing, Cache Diagnostics reports no divergence. The mechanism is working by every check in this guide. And the monthly cost is flat, or higher than before you enabled caching. Nothing is broken. Caching engaged and did not pay.

The reason is that a cache read is cheap but a cache write, on a provider that charges a write premium, is billed above the normal input rate. That premium only comes back if enough reads follow before the entry expires. A cache that writes far more often than it reads loses money while every counter says “working.” The break-even sits a little above one read per write once the write premium is included; below it, caching costs more than doing nothing. The full derivation, covering the write premium, the read discount, the TTL deadline and where a given workload lands, is in Prompt Caching Explained. Here’s how to spot an economics failure so you don’t file it as a bug:

  • Reuse slower than the TTL. Entries expire between calls, so each request writes a fresh one and few are ever read. This is the case that passes a tight back-to-back test and then underperforms in production, where traffic is spread out. It is the TTL row of the diagnostics matrix above.
  • Sparse or bursty traffic. A prefix hit a couple of times an hour never accumulates enough reads inside a short entry lifetime to clear break-even.
  • Parallel calls with no warm-up. Several requests that share a prefix but all fire before any of them has written the entry each pay to write it, and the hit rate collapses. Thomson Reuters Labs recorded a 4.2% hit rate this way, and costs above their fully-uncached baseline, before adding a warm-up request ahead of the parallel batch (write-up).
  • Output-dominated cost. Caching only touches input processing. If your spend is mostly long generations, a working input cache barely moves the total no matter how high the read counter climbs.

The fixes here are about timing and traffic: cluster requests that share a prefix so they land inside the TTL, warm the entry once before fanning out parallel work, request a longer cache duration where the provider supports it, or accept that this particular workload doesn’t earn back the write and turn caching off for it. That’s a valid outcome.

Keep it from silently regressing

Every failure in this guide is silent by construction: the request succeeds, no error is raised, and only a counter moves. That has a consequence beyond the initial fix. A cache you diagnosed and repaired today can break again next month from a change that looks entirely unrelated. A new per-request field added to the system prompt, a tool reordered in a refactor, a debug timestamp left in after an incident, a gateway upgrade that reshapes requests. None of those raise an error. The only thing that catches them is the cache-read counter you now know how to read, watched over time rather than once.

So the last step of troubleshooting is to stop troubleshooting reactively. Wire the read counter into whatever you already monitor, and track two signals: the cached-token share of your input, which should hold near the stable-prefix fraction you expect, and the cost-per-request at steady usage, which should stay down. When either drifts, you are back at the top of the decision tree, but now you catch it as a metric moving.

On a single provider you can read those signals straight from the usage object. If you call several providers, they scatter across different billing pages with different field names, which is where pulling them into one place helps; tracking AI costs across providers covers that, and CostCompass pulls billed spend from your connected providers into one running total, split by model, so a bill that creeps back up is visible when you look; cached-token share itself still comes from each provider’s usage data.

Next steps

A stuck prompt cache is a diagnosis, not a mystery. Read the write counter, then the read counter, then compare the reads against the writes, and every failure lands in one of three places: nothing was stored (write-side), something was stored but your prefix diverged in one specific segment (read-side), or it stored and read as designed but the reuse was too thin to pay (economics-side). The provider already reports everything you need to place the failure; the skill is knowing which counter answers which question, and in what order.

From here:

  • Understand the economics behind the third region. Prompt Caching Explained derives the write premium, read discount, and break-even that decide whether a working cache saves.
  • Read it off the Claude Console. How to read Claude’s cache usage dashboard runs this same diagnosis visually, covering the read ratio, write amortization and the Cache diagnostics panel that names the divergent segment, without adding any logging.
  • Revisit how you turned it on. How to Enable Prompt Caching has the exact per-provider markers and the verify-once procedure, which is where most write-side and read-side fixes are applied.
  • Watch it over time. Tracking AI costs across providers covers putting billed spend from every provider into one total, so a bill that creeps back up is visible when you look.

Frequently asked questions

How do I check whether prompt caching is working?
Read the cache counters in the response's usage object. The provider reports them on every call. Anthropic returns cache_creation_input_tokens (written) and cache_read_input_tokens (served from cache); OpenAI GPT-5.6+ Responses calls report cache_write_tokens and cached_tokens under usage.input_tokens_details; older response shapes expose cached_tokens under usage.prompt_tokens_details; Amazon Bedrock returns cacheWriteInputTokens and cacheReadInputTokens; Google Gemini reports a cached-content token count under usageMetadata. Send the same-prefix request twice: the first call should show a write, the second should show a read. A read count that grows across repeated calls is proof the mechanism engaged. Everything else in troubleshooting is deciding which counter is wrong.
My cache write count is high but cache reads are always zero. What does that mean?
The provider stored your prefix and then never matched it. Usually that is a read-side failure: something in the leading part of your request changed between calls, so the byte-exact prefix match ended before it reached your cached content. Or, if nothing changed, the entry expired before the repeat call. When a change is the cause, it lives in exactly one request segment, the model, the system prompt, the tool definitions, or the earlier message history. Diff two "identical" requests and look for a per-request value near the front (a timestamp, a request id, a reordered tool list, an edited earlier message). On the Claude API, the Cache Diagnostics beta names the divergent segment for you.
Both my cache-write and cache-read counts are zero. Why is nothing being cached?
Nothing was stored, which is a write-side failure with two usual causes. Either the cacheable prefix is below the model's minimum cacheable size — under that minimum the provider silently stores nothing and returns a normal response — or the cache marker never took effect because of a formatting or gateway mistake (for example, cache_control attached at the message level on a plain-string system field instead of on the content block, or an SDK or proxy that dropped the marker in translation). Confirm the prefix clears the model's minimum, then confirm the marker is on the content block your provider expects.
Why does changing one small field break my whole prompt cache?
Because the match is a byte-exact comparison of the leading prefix, evaluated in order from the first token, and it stops at the first token that differs. Everything after that first difference is treated as uncached, even if it is identical to a previous request. So a single volatile value near the front — a timestamp in the system prompt, a request id, a reordered JSON key, an extra whitespace from a template change — invalidates the entire cacheable region behind it. The fix is structural: keep the leading prefix byte-for-byte constant and move anything per-request to the end of the request.
How do I find where my prompt prefix diverged between requests?
Compare two consecutive requests that should have matched. On the Claude API, the Cache Diagnostics beta (header cache-diagnosis-2026-04-07) does this for you: pass the previous response id and it returns a cache_miss_reason naming the first segment that changed: model_changed, system_changed, tools_changed, or messages_changed. On providers without a diagnostics tool, reproduce the same comparison manually: serialize the exact bytes of both requests and diff them, working front to back, until you find the first difference. The first difference is the whole problem; fixes further back are hidden behind it.
Prompt caching is working but my costs didn't go down. Is it broken?
Not necessarily. A cache can read correctly and still cost you money. Checking that it engaged and measuring that it paid are separate things. On a provider that charges a write premium, the premium you pay to store a prefix only pays back if enough cheap reads follow before the entry expires. If your reuse is sparse — a slow trickle of requests, entries expiring between calls, or parallel calls that all write before any reads — the reads can be real and the bill can still rise. The Prompt Caching Explained guide covers the break-even math that decides it.

About the author

Joubert Berger builds CostCompass, a spend-intelligence dashboard that pulls usage from AI and compute providers into one month-to-date total, a forecast, and a per-provider breakdown. This guide reflects how CostCompass reads each provider's own usage API — see the security model for how your keys are handled.

See whether caching moved your bill

A cache regression shows up first as a cache-read count that falls back to zero, and later as a bill that creeps up. CostCompass pulls billed spend from your connected providers into one running total, split by model, so you see that creep when you look instead of in an end-of-month invoice.