Verifying Cache Hits with cache_read_input_tokens
Adding a cache_control breakpoint does not, by itself, prove your requests are actually hitting the cache.
Search across all documentation pages
Adding a cache_control breakpoint does not, by itself, prove your requests are actually hitting the cache.
The only reliable signal is the usage object on each response, specifically cache_read_input_tokens and cache_creation_input_tokens.
This page shows how to read those fields correctly, how to build a lightweight check into your own code, and what values actually mean success versus failure.
response.usage carries four token counts that matter for caching: input_tokens, output_tokens, cache_creation_input_tokens, and cache_read_input_tokens.
A cache write shows up as nonzero cache_creation_input_tokens; a cache hit shows up as nonzero cache_read_input_tokens.
Because caching fails silently - a missed cache never raises an error - checking these fields on every request, not just during development, is the only way to know your caching setup is actually working in production.
This page builds a small reusable helper for that check and walks through the values you should expect at each stage of a request's lifecycle.
Quick-reference recipe card - copy-paste ready.
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=200,
system=[
{
"type": "text",
"text": "You are Acme Corp's support assistant." * 200,
"cache_control": {"type": "ephemeral"},
}
],
messages=[{"role": "user", "content": "What's your refund window?"}],
)
usage = response.usage
print("input_tokens:", usage.input_tokens)
print("cache_creation_input_tokens:", usage.cache_creation_input_tokens)
print("cache_read_input_tokens:", usage.cache_read_input_tokens)When to reach for this:
cache_control breakpoint, to confirm it's working at all.import anthropic
client = anthropic.Anthropic()
SYSTEM_TEXT = "You are Acme Corp's support assistant. Reference material follows.\n" + ("Policy detail. " * 300)
def ask_with_cache_check(question: str) -> str:
"""Send a cached request and report exactly what happened with the cache."""
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=300,
system=[
{
"type": "text",
"text": SYSTEM_TEXT,
"cache_control": {"type": "ephemeral"},
}
],
messages=[{"role": "user", "content": question}],
)
usage = response.usage
if usage.cache_read_input_tokens > 0:
status = "HIT"
elif usage.cache_creation_input_tokens > 0:
status = "WRITE (first call or expired TTL)"
else:
status = "NO CACHE ACTIVITY (breakpoint too small or missing)"
print(
f"[{status}] read={usage.cache_read_input_tokens} "
f"write={usage.cache_creation_input_tokens} "
f"input={usage.input_tokens} output={usage.output_tokens}"
)
return response.content[0].text
ask_with_cache_check("What's your refund window?")
ask_with_cache_check("Do you ship internationally?")What this demonstrates:
HIT.response.usage is populated on every successful messages.create() call, whether or not caching is configured at all.cache_creation_input_tokens counts tokens written to the cache on this call - nonzero on a fresh write or after a TTL expiry.cache_read_input_tokens counts tokens read from an existing cache entry - nonzero only on an actual hit.input_tokens reflects the tokens processed normally, outside of any cached segment - this is usually small once caching is working, since it's just the uncached tail of your prompt.cache_creation_input_tokens value.| Scenario | cache_creation_input_tokens | cache_read_input_tokens | input_tokens |
|---|---|---|---|
| First call ever (cold cache) | High | 0 | Low (uncached tail only) |
| Repeat call, cache still warm | 0 | High | Low (uncached tail only) |
| Cache expired, rewritten | High | 0 | Low |
No cache_control used at all | 0 | 0 | Full prompt size |
| Prefix changed, cache invalidated | High (again) | 0 | Low |
# usage fields are plain integers on the SDK's typed usage object -
# safe to compare, sum, or log directly without extra parsing.
usage = response.usage
total_cached_prefix_tokens = usage.cache_read_input_tokens + usage.cache_creation_input_tokens
hit_ratio = (
usage.cache_read_input_tokens / total_cached_prefix_tokens
if total_cached_prefix_tokens
else 0.0
)| Field | Type | Description |
|---|---|---|
usage.input_tokens | int | Tokens processed normally, outside any cached prefix. |
usage.output_tokens | int | Tokens generated by Claude in the response. |
usage.cache_creation_input_tokens | int | Tokens written to the cache on this call. |
usage.cache_read_input_tokens | int | Tokens read from an existing cache entry on this call. |
input_tokens and assuming caching is "just working." input_tokens alone doesn't tell you whether a cache read happened - you need the two cache-specific fields. Fix: always log all four usage fields together, not just total tokens.cache_read_input_tokens == 0 is expected, not a bug. Fix: always test with at least two consecutive calls sharing the same prefix.cache_creation_input_tokens means something is wrong. A write is completely normal on the first call or after a TTL expiry - it isn't itself an error signal. Fix: only treat "no cache activity of either kind" or "an unexpected write on what should be a repeat call" as a red flag.cache_read_input_tokens in the live system. Fix: emit these fields to your metrics or logging pipeline on every request, not just during development.| Alternative | Use When | Don't Use When |
|---|---|---|
| Per-request usage check (this page) | You need ground truth on every single call | You only need a rough sense of overall spend |
| Aggregate billing dashboard review | Spot-checking overall spend trends periodically | You need to catch a regression the moment it happens |
| Manual print-based debugging | One-off local investigation of a specific prompt | Any production or CI setting - not durable or queryable |
usage.cache_read_input_tokens. A nonzero value there means the request successfully reused a previously cached prefix.
usage.cache_creation_input_tokens. A nonzero value means this call cached content that wasn't previously cached, or that had expired.
Yes - the first call to any given prefix is always a write, not a hit. You need at least two calls with an identical prefix to see a read.
No. A cache miss just looks like a normal request; the only difference is which usage fields are nonzero. There is no separate error signal.
Either no cache_control breakpoint was set, or the content marked for caching was too small to be worth caching. Check that your breakpoint is actually present in the request.
Yes, if you have multiple breakpoints - an earlier segment can hit an existing cache entry while a later segment writes a new one in the same request.
Divide cache_read_input_tokens by the sum of cache_read_input_tokens and cache_creation_input_tokens for a given prefix, tracked over a window of requests.
In production. Caching regressions are silent by design - the only way to catch one in the wild is to be watching these fields continuously, not just when you first set caching up.
Not by itself - check cache_read_input_tokens directly. A low input_tokens combined with a nonzero cache_creation_input_tokens just means you're mid-write, not necessarily hitting cache yet.
Send the exact same request twice in a row and check that the second response shows nonzero cache_read_input_tokens. If it doesn't, the breakpoint or prefix isn't stable between the two calls.
Stack versions: Written against the Claude model lineup current as of ~June 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (the default), and Claude Haiku 4.5 - and the official
anthropicPython SDK (latest 0.x release). Model names, SDK versions, and pricing move quickly - verify current specifics at platform.claude.com/docs before relying on them.
Reviewed by Chris St. John·Last updated Jul 19, 2026