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.
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.
Summary
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.
Recipe
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:
- Right after you add your first
cache_controlbreakpoint, to confirm it's working at all. - In production logging or metrics, to catch a caching regression the moment it happens.
- When debugging a bill that's higher than expected for a supposedly-cached workload.
- Any time you change prompt content and want to confirm the cache is still hitting afterward.
Working Example
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:
- A three-way status check: cache hit, cache write, or no cache activity at all.
- Calling the same function twice in a row with an identical system block - the second call should report
HIT. - Surfacing all four relevant usage fields together, so a regression is visible in one glance rather than requiring separate lookups.
- A pattern you can drop into request logging without changing your application's actual response handling.
Deep Dive
How It Works
response.usageis populated on every successfulmessages.create()call, whether or not caching is configured at all.cache_creation_input_tokenscounts tokens written to the cache on this call - nonzero on a fresh write or after a TTL expiry.cache_read_input_tokenscounts tokens read from an existing cache entry - nonzero only on an actual hit.input_tokensreflects 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.- None of these fields raise a warning or error on a miss; a cache miss looks exactly like a normal request that happens to also carry a
cache_creation_input_tokensvalue.
Reading the Four Fields Together
| 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 |
Python Notes
# 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
)Parameters & Return Values
| 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. |
Gotchas
- Only checking
input_tokensand assuming caching is "just working."input_tokensalone 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. - Testing caching with a single request. The very first call to a new prefix is always a write, never a hit - testing with one call and seeing
cache_read_input_tokens == 0is expected, not a bug. Fix: always test with at least two consecutive calls sharing the same prefix. - Assuming a nonzero
cache_creation_input_tokensmeans 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. - Not logging usage in production, only in local testing. Caching regressions from a prompt change often ship unnoticed because nobody is watching
cache_read_input_tokensin the live system. Fix: emit these fields to your metrics or logging pipeline on every request, not just during development. - Comparing hit rate across requests with different prefixes. A drop in aggregate hit rate can mean one specific prefix started missing, not that caching broadly "stopped working." Fix: tag your cache metrics by which prompt/endpoint they came from so you can isolate the affected prefix.
Alternatives
| 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 |
FAQs
Which field tells me a request was a cache hit?
usage.cache_read_input_tokens. A nonzero value there means the request successfully reused a previously cached prefix.
Which field tells me a request wrote to the cache?
usage.cache_creation_input_tokens. A nonzero value means this call cached content that wasn't previously cached, or that had expired.
Is it normal for the very first request to a new prompt to show zero cache_read_input_tokens?
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.
Does a cache miss raise an exception or warning?
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.
What does it mean if both cache_creation_input_tokens and cache_read_input_tokens are zero?
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.
Can a single response show both cache_creation_input_tokens and cache_read_input_tokens as nonzero?
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.
How do I build a hit rate metric from these fields?
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.
Should I log these fields in production, or only during development?
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.
If input_tokens is low, does that mean caching is working?
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.
What's the fastest way to confirm caching is set up correctly at all?
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.
Related
- How Prompt Caching's Prefix Match Actually Works - why a hit requires byte-for-byte identical content.
- Prompt Caching Basics - the first breakpoint this page verifies.
- Debugging Silent Cache Invalidators in Long Prompts - what to do when this check reveals a problem.
- Prompt Cache Miss Storms - Diagnosing Sudden Cost and Latency Spikes - production incident response for a widespread caching failure.
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.