Read Cache Usage
Three Fields, One Total
Caching changes how input tokens are counted, so the usage fields need careful reading. Every response reports three input values (Lesson 44 introduced the basics):
| Field | Meaning |
|---|---|
input_tokens | Input after the last breakpoint, which was not cached |
cache_creation_input_tokens | Input written to the cache by this request |
cache_read_input_tokens | Input read from the cache by this request |
The total input for the request is the sum of all three:
total input = cache_read_input_tokens + cache_creation_input_tokens + input_tokens
The most common mistake is to read input_tokens as the total. It is only the uncached tail of the prompt. With a large cached document, input_tokens can be a tiny fraction of what Claude actually read.
What Each Field Costs
Each field is priced differently (Lesson 121):
| Field | Price relative to base input |
|---|---|
input_tokens | 1× |
cache_creation_input_tokens, 5-minute TTL | 1.25× |
cache_creation_input_tokens, 1-hour TTL | 2× |
cache_read_input_tokens | 0.1× (0.05× on Opus 5.5 and Sonnet 5.5) |
The usage object can also break the writes down by TTL, in a cache_creation field, with ephemeral_5m_input_tokens and ephemeral_1h_input_tokens. Their sum equals cache_creation_input_tokens. Use the breakdown when you mix TTLs, since the two cost different amounts.
Two Useful Measures
| Measure | Formula | What it tells you |
|---|---|---|
| Hit rate | read / total input | How much of the input came from the cache |
| Effective cost | The sum of each field times its price | What the request actually cost |
A high hit rate on a request that wrote nothing is the ideal pattern. A low hit rate means the prefix changed, or the request was below the minimum, or the entry expired. Lessons 126 and 127 explain how to find out which.
Practical Example: A Cache Report
This module turns usage fields into a cost report. Its main function works on any usage object, so the example runs offline with a fabricated usage record, and the same function works on live responses.
"""cache_report.py: turn usage fields into hit rate and cost, with per-model prices."""
from types import SimpleNamespace
# USD per million tokens, as listed on the pricing page
PRICES = {
"claude-sonnet-5-5": {"input": 2.00, "write_5m": 2.50, "write_1h": 4.00, "read": 0.10},
"claude-opus-5-5": {"input": 4.00, "write_5m": 5.00, "write_1h": 8.00, "read": 0.20},
}
def write_breakdown(usage) -> tuple[int, int]:
"""Return (5-minute writes, 1-hour writes), using the TTL breakdown when it is present."""
detail = getattr(usage, "cache_creation", None)
if detail is not None:
return detail.ephemeral_5m_input_tokens, detail.ephemeral_1h_input_tokens
return usage.cache_creation_input_tokens, 0
def total_input(usage) -> int:
return usage.input_tokens + usage.cache_creation_input_tokens + usage.cache_read_input_tokens
def hit_rate(usage) -> float:
total = total_input(usage)
return usage.cache_read_input_tokens / total if total else 0.0
def cost(model: str, usage) -> float:
prices = PRICES[model]
writes_5m, writes_1h = write_breakdown(usage)
return (usage.input_tokens * prices["input"]
+ writes_5m * prices["write_5m"]
+ writes_1h * prices["write_1h"]
+ usage.cache_read_input_tokens * prices["read"]) / 1_000_000
def cost_without_caching(model: str, usage) -> float:
return total_input(usage) * PRICES[model]["input"] / 1_000_000
def report(model: str, usage) -> str:
with_cache = cost(model, usage)
without = cost_without_caching(model, usage)
saved = 100 * (1 - with_cache / without) if without else 0.0
return (f"total input tokens: {total_input(usage)}\n"
f"hit rate: {100 * hit_rate(usage):.2f}%\n"
f"cost with caching: ${with_cache:.4f}\n"
f"cost without caching: ${without:.4f}\n"
f"saved: {saved:.0f}%")
if __name__ == "__main__":
# A fabricated usage record: 50 new tokens, 100,000 read from the cache, nothing written
usage = SimpleNamespace(
input_tokens=50,
cache_creation_input_tokens=0,
cache_read_input_tokens=100_000,
cache_creation=SimpleNamespace(ephemeral_5m_input_tokens=0, ephemeral_1h_input_tokens=0),
)
print(report("claude-sonnet-5-5", usage))
# On a live response, the same functions work directly:
# message = client.messages.create(...)
# print(report("claude-opus-5-5", message.usage))
Output:
total input tokens: 100050
hit rate: 99.95%
cost with caching: $0.0101
cost without caching: $0.2001
saved: 95%
How It Works
total_input()adds all three fields. It is the number to use for context-size checks and rate-limit thinking.hit_rate()divides the cache reads by the total. The example's 99.95% means almost the whole prompt came from the cache.write_breakdown()uses the TTL detail when the response includes it, so 5-minute and 1-hour writes are priced separately. It falls back to the total when the detail is absent.cost()multiplies each field by its own price, using the per-model table. The reads are cheap, and the 50 uncached tokens are charged at the full input rate.cost_without_caching()prices the same total at the normal input rate, which gives the baseline for the savings.- The numbers check out by hand. The 100,000 read tokens cost 0.1 million times $0.10, which is $0.01. The 50 new tokens cost $0.0001. Together that is $0.0101, against $0.2001 for the full 100,050 tokens at $2.00 per million.
Note: the fabricated record is useful for testing the functions. Live numbers will differ, and the prices in
PRICESmust be kept current with the pricing page, because prices can change.
Common Mistakes
Reading input_tokens as the Total
What happens: costs and context sizes look far smaller than they really are.
Fix: always add the three fields.
Ignoring Writes
What happens: a request that wrote a large entry looks cheap, because it seems to have read nothing.
Fix: include cache_creation_input_tokens in cost calculations.
Using One Price for All Fields
What happens: writes and reads are costed as if they were normal input, so the report is wrong in both directions.
Fix: price each field separately, using the table.
Hardcoding Prices
What happens: prices change, and reports silently drift from the real bill.
Fix: keep prices in configuration, and review them when the pricing page changes.
Ignoring the Model-Specific Read Multiplier
What happens: the cost of reads on Opus 5.5 is overstated by a factor of two if you assume 0.1×.
Fix: use the per-model read price from the pricing page.
Best Practices
- Log all three fields for every request.
- Report hit rate and effective cost separately for each feature.
- Price writes and reads separately, using current, per-model prices.
- Track the TTL breakdown when you use both TTLs.
- Alert on hit rates that drop suddenly, since a drop usually means a prompt change (Lesson 127).
Exercise
- Change the usage record so
cache_creation_input_tokensis 100,000 andcache_read_input_tokensis 0. What does the report show, and what does that tell you about a first request? - Set
ephemeral_1h_input_tokensto 100,000 andephemeral_5m_input_tokensto 0, withcache_creation_input_tokensalso at 100,000. How much does the write cost change? - Compute the hit rate for a request with 40,000 read tokens, 0 written tokens, and 10,000 uncached tokens.
- Explain why the function uses
getattr(usage, "cache_creation", None)instead of accessing the attribute directly.
Step 2: with the 1-hour TTL, the write is priced at $4.00 per million for Sonnet 5.5 instead of $2.50, so the write cost rises from $0.25 to $0.40 for 100,000 tokens. The 5-minute and 1-hour writes are priced differently, which is why the breakdown matters. (In the Opus 5.5 table, the difference is $8.00 against $5.00.)
Step 4: not every response includes the cache_creation breakdown, and older usage records or responses from other providers may not have it. getattr with a default returns None when the attribute is missing, and the function then falls back to the combined total. Accessing the attribute directly would raise an error in that case, and make the report fail on valid responses.