The cache line

A prompt cache is a prefix match, and almost every way of getting it wrong looks exactly like getting it right.

call A · 09:41:02
tools[]2.1k tok
"today is 09:41:02"volatile
system9.9k tok
messages0.9k tok
miss → write → $0.0450 for the prefix
call B · 09:41:09
tools[]2.1k tok
system9.9k tok
"today is 09:41:09"volatile
messages0.9k tok
hit → read → $0.0036 for the same tokens
the cached prefix
served from cacheread fresh, every callinvalidated by the line above it
A poisoned prefix. Two calls, seconds apart, against the same model and the same system prompt. The only difference is four words near the top — a timestamp — and it costs 12.5× more per call than the version below it. Nothing in the response, the logs or the dashboard tells you which one you shipped.

Prompt caching is the rare optimisation that is both enormous and nearly free: one field, in the right place, and the largest part of your prompt costs a tenth as much. It is also the rare optimisation that fails silently. There is no error, no warning, and no field in the response that says "your cache never once hit." There is only the invoice, arriving three weeks later, being wrong in a way nobody can explain.

I spent an afternoon last spring convinced our caching was working because the code said cache_control in it. It was working the way a smoke alarm with no battery is working.

Suppose 5,000 requests a day hit the same Sonnet 5 prompt, and the frozen part of that prompt — 12k tokens of system prompt and tool definitions — never changes. Uncached, you pay $7,380 a month. With one cache_control breakpoint in the right place, you pay $2,521 — a 2.93× difference, or $4,859 a month you were setting on fire.

Holding fixed: 900 volatile tokens and 700 output tokens per call, traffic spread over 10 active hours, 5-minute TTL, 30-day month, list prices.

The mechanism is worth understanding properly rather than by cargo cult, because every failure mode falls out of one sentence: a prompt cache is a prefix match on bytes. Everything else — the TTLs, the multipliers, the minimum lengths, the four-breakpoint limit — is detail hanging off that sentence.

Three blocks, one order

Before anything is cached, the request is assembled. Three regionsThe order is fixed by the API and is not configurable. Reordering your own code changes nothing about it., always in this order: tools, then system, then messages. The result is hashed as a byte string, and the hash is the cache key.Four cache_control breakpoints per request, maximum. In practice one, at the end of the stable region, does almost all the work — a second only earns its place when you have two genuinely different reuse lifetimes in one prompt.

That gives you the only rule you actually need: stable content first, volatile content last. Not "stable content marked with a breakpoint" — stable content physically earlier in the rendered prompt. A marker cannot protect bytes that sit above something that changes.

request.py
tools = sorted(TOOL_DEFS, key=lambda t: t["name"])
system = [{"type": "text", "text": FROZEN_PROMPT}]
system[-1]["cache_control"] = {"type": "ephemeral"}
messages = [{"role": "user", "content": f"{now()}\n{question}"}]
The same request, region by region. Hover or focus any line of code to light up the region it belongs to, and any block to light up its code. Click to pin, so the highlight survives your mouse leaving. Non-active regions dim rather than disappear — you should always be able to read the whole thing.

The write is an option

A cache write costs more than not caching. That is the part people skip, and it is the part that makes the arithmetic interesting rather than obvious.

Ccached(n)  =  wpone write  +  rp(n1)reads,w=1.25,  r=0.1C_{\text{cached}}(n) \;=\; \underbrace{w \cdot p}_{\text{one write}} \;+\; \underbrace{r \cdot p \cdot (n-1)}_{\text{reads}}, \qquad w = 1.25,\; r = 0.1

A write costs 1.25× list price; every read after it costs 0.1×. So caching pays for itself from the 2nd request onward.

So you are buying an option. You pay a small premium up front for the right to re-read those tokens cheaply, and the option expires — five minutes by default, an hour if you pay double for the write. What matters is how many times you exercise it before it lapses.

And the option can expire worthless. A hit resets the TTL, so a warm entry survives indefinitely on its own traffic — but if calls arrive further apart than the TTL, every single one finds an expired entry, and caching becomes a pure surcharge with no upside.Which is the honest argument for the one-hour TTL. It is not "better"; it is the same bet with a longer expiry, priced accordingly. Take it when your traffic has gaps of five to sixty minutes, and not otherwise. The chart below crosses over for exactly this reason: at the left-hand end, not caching wins.

cache entry
∅ no entry12k tok
ttl
this call pays
nothing yet
Running total after this call: $0.00

Cold. No entry exists for this prefix hash.

1 / 7
One entry, cradle to grave. Step through with the arrows, or focus the figure and use the left and right keys. Auto-play only starts if you ask it to, and stops when this scrolls out of view. The last state is the one that quietly costs you money: a changed byte does not update the entry, it makes a different entry, and the old one ages out having never been read.

Across a day of realistic traffic the difference is not subtle: the effective price of the same prefix tokens tracks under a working cache and under a poisoned one — flat, because a poisoned prefix never amortises anything.

$1$10$100$1,000101001,00010,00050,000.No cachePoisoned prefixStable prefix, 5mStable prefix, 1h
Cost per day against volume, log–log. Left of the crossover, calls arrive further apart than the TTL, every one misses, and both cached lines sit on top of the poisoned one — that region is not a bug in the chart, it is the honest answer. Right of it they collapse toward each other, because nearly every call is a read and the write premium stops mattering. The dashed line never improves at any volume: a poisoned prefix is not "caching that isn't helping," it is a 25% surcharge you opted into. Toggle a series off to compare two directly.

How a prefix gets poisoned

Nothing so far is hard. The hard part is that the failure is invisible, and every plausible-looking piece of code below causes it.

request
tools[]2.1k tokTool definitions render first. Adding, removing or reordering one invalidates everything.
system9.9k tokThe frozen instructions. This is what you want cached.
messagesuser turnBelow the breakpoint. Never cached, and it never needed to be.
No breakpoint set. The whole prompt is read fresh, every call.
this call costs
$0.0360list price
cached prefixread freshinvalidated

The prompt renders in one fixed order: tools, then system, then messages.

The API assembles your request in a fixed order — tools, then system, then messages — and hashes it as a byte string. That order is the whole game. Everything that follows is a consequence of it.

request
tools[]2.1k tokTool definitions render first. Adding, removing or reordering one invalidates everything.
system9.9k tokThe frozen instructions. This is what you want cached.
messagesuser turnBelow the breakpoint. Never cached, and it never needed to be.
◇ marks the cache_control breakpoint. Prefix = 12k tokens.
this call costs
$0.0360list price
cached prefixread freshinvalidated

A cache_control breakpoint marks the end of the reusable prefix.

A cache_control marker on the last system block draws a line. Everything above it is a prefix the API can look up by hash. Everything below is fresh text it has to read.

request
tools[]2.1k tokTool definitions render first. Adding, removing or reordering one invalidates everything.
system9.9k tokThe frozen instructions. This is what you want cached.
messages900 tok, volatileBelow the breakpoint. Never cached, and it never needed to be.
◇ marks the cache_control breakpoint. Prefix = 12k tokens.
this call costs
$0.04501.25× — cache write
cached prefixread freshinvalidated

First call: a write. 1.25× list price on the prefix.

The first call pays a premium — 1.25× the list rate on those prefix tokens — to store the entry. On a 12k-token prefix that is $0.0450 instead of $0.0360. You are buying an option.

request
tools[]2.1k tokTool definitions render first. Adding, removing or reordering one invalidates everything.
system9.9k tokThe frozen instructions. This is what you want cached.
messages900 tok, volatileBelow the breakpoint. Never cached, and it never needed to be.
◇ marks the cache_control breakpoint. Prefix = 12k tokens.
this call costs
$0.00360.1× — cache read
cached prefixread freshinvalidated

Every later call inside the window: a read, at 0.1× list price.

Every subsequent call inside the TTL window exercises that option at 0.1× $0.0036 for the same tokens. Two calls and you are already ahead. This is the entire mechanism.

request
tools[]2.1k tokTool definitions render first. Adding, removing or reordering one invalidates everything.
datetime.now()changes every callA single volatile byte above the breakpoint invalidates the whole prefix.
system9.9k tokThe frozen instructions. This is what you want cached.
messagesuser turnBelow the breakpoint. Never cached, and it never needed to be.
No breakpoint set. The whole prompt is read fresh, every call.
this call costs
$0.04501.25× — cache write
cached prefixread freshinvalidated

One timestamp at the top and the prefix hash changes on every call.

Now interpolate a timestamp into the top of the system prompt. The bytes before the breakpoint differ on every request, so every request is a miss, and every miss is a fresh write. You now pay 1.25× forever. The marker is still there. It has never once been read.

request
tools[]2.1k tokTool definitions render first. Adding, removing or reordering one invalidates everything.
system9.9k tokThe frozen instructions. This is what you want cached.
messages900 tok, volatileBelow the breakpoint. Never cached, and it never needed to be.
◇ marks the cache_control breakpoint. Prefix = 12k tokens.
this call costs
$0.00360.1× — cache read
cached prefixread freshinvalidated

Move the volatile content below the breakpoint. Nothing else changes.

The fix is not a new parameter. It is moving four words down the page: put the timestamp in the user turn, below the breakpoint, where it invalidates nothing. Same information, same tokens, 12.5× less money.

The tell is always the same, and it is one field: usage.cache_read_input_tokens. If it is zero across repeated requests that should share a prefix, something above your breakpoint is moving.Do not read input_tokens as "the prompt size". It is the uncached remainder only. Total prompt size is input_tokens + cache_creation_input_tokens + cache_read_input_tokens, and an agent that ran for an hour showing 4k input_tokens is a working cache, not a small prompt.

naivestable
system
Today is 2026-07-14.
You are a support agent…9.9k tok
messages
user turn
prefix hash changes daily — or hourly, or per request
system
You are a support agent…9.9k tok
messages
Today is 2026-07-14.
user turn
prefix hash is frozen — the date rides below the breakpoint
Same information, different position. Drag the divider, click anywhere in the figure, or focus the handle and use the arrow keys. Both versions put the current date in front of the model. Only one of them keeps the prefix hash stable, and it is not the one that reads more naturally.
prompt.py — stable prefix
SYSTEM = load_frozen_prompt()  # no interpolation, ever

resp = client.messages.create(
  model="claude-sonnet-5",
  system=[{"type": "text", "text": SYSTEM,
           "cache_control": {"type": "ephemeral"}}],
  tools=sorted(TOOLS, key=lambda t: t["name"]),
  messages=[
      *history,
      {"role": "user",
       # Volatile context goes here, below the breakpoint.
       "content": f"Today is {date.today()}.\n\n{question}"},
  ],
  max_tokens=16000,
)
assert resp.usage.cache_read_input_tokens > 0  # the only real check
The diff is four lines. Switch tabs to compare. The naive version is not sloppy code — it is the version most people write first, and it reads better. It also invalidates the prefix on every request, which the highlighted lines are doing on purpose so you can see them.

The list of things that do this is short and boring, which is what makes it easy to miss in review: a date or timestamp in the system prompt; a UUID or request id; json.dumps without sort_keys=True; iterating a set; a per-user tool list; conditional system sections, where every combination of flags is a distinct prefix; and — the one that gets everybody — switching models mid-conversation, because caches are model-scoped.The pattern that saves you here is architectural rather than clever: freeze the system prompt as a constant at import time and make interpolating into it impossible rather than merely unwise. If a value has to reach the model mid-conversation, put it in the messages array, not in the system field.

The floor nobody mentions

There is one more way to have a perfectly correct, perfectly stable, perfectly marked prefix that never caches: it is too short.

Modelidinput $/MTokoutput $/MTokmin. cacheable prefix
Opus 5claude-opus-5$5.00$25.00512 tok
Sonnet 5claude-sonnet-5$3.00$15.001,024 tok
Haiku 4.5claude-haiku-4-5$1.00$5.004,096 tok
The minimum is not monotonic. A 3,000-token prefix caches on Opus 5 and Sonnet 5 and silently does not on Haiku 4.5, which needs 4,096. There is no error and no warning — just cache_creation_input_tokens: 0 forever.

Below the model's minimum, the breakpoint is accepted, the request succeeds, and cache_creation_input_tokens comes back as 0 forever. And the minimum is not monotonic across the range — the newest model has the lowest floor, so a prefix that caches happily on one model can go inert when you route the same traffic to a cheaper one. is the Opus 5 floor drawn against the Haiku 4.5 floor.

TTLwritereadbreak-even
5 minutes (default)1.25×0.1×2 requests
1 hour0.1×3 requests
Choose the hour only when traffic has gaps longer than five minutes
Two numbers, one decision. The one-hour TTL is not better; it is a different bet. You pay double for the write to buy an entry that survives idle gaps.
Break-even, by TTL. Both thresholds are small. If your prefix is stable and long enough, and more than a handful of calls share it inside the window, the decision is already made.

Do the arithmetic yourself

Everything above is one pure function with about forty lines in it. Here it is with the sliders attached — the same function that drew every chart, table and sparkline on this page.

Strategymultiplier$ / day$ / month
No cache1.00×$246$7,380
Poisoned prefix0.85×$291$8,730
Stable prefix, 5m2.93×$84.04$2,521
Stable prefix, 1h2.93×$84.07$2,522
Saved by Stable prefix, 5m$162$4,859
Read the second row first. A poisoned prefix is the only configuration that is worse than not caching at all — it pays the write premium on every call and never once reads. Everything here recomputes synchronously from one pure model() function.

At 5,000 requests a day against a 12k-token prefix, Stable prefix, 5m costs $2,521 a month against $7,380 uncached — saving $4,859, a 2.93× improvement.

Assumes uniform arrivals across the active window, so a warm entry is refreshed by its own traffic and only one write is needed — unless the mean gap between calls exceeds the TTL, in which case every call misses. Also assumes list prices with no negotiated discount, a 30-day month, and a prefix that is byte-identical on every call. Real traffic is bursty; treat these as the shape of the answer, not the answer.

$1$10$100$1,000101001,00010,00050,000.No cachePoisoned prefixStable prefix, 5mStable prefix, 1h
Move something. Pick a preset to see a shape, then drag any slider to go off-preset. Push the stable prefix below the model's minimum and watch all four strategies collapse onto the same number — that is the inert case, and it is worth seeing on purpose once so you recognise it in the wild. The link button copies the URL with your exact parameters in the hash.

What I actually changed

Three things, none of them clever.

I moved the timestamp out of the system prompt and into the user turn — the single highest-value four-line diff I have ever written, worth $4,859 a month at our volume. I froze the system prompt as a module-level constant so that interpolating into it requires editing the constant, which requires a review. And I added one assertion to the smoke test: cache_read_input_tokens > 0. That assertion has caught the regression twice since.

The last one is the real lesson. Prompt caching does not need monitoring or dashboards or a cost anomaly detector. It needs one boolean, checked once, in a test that already runs — because the only thing that makes this failure expensive is that nothing was watching for it.

Thanks to everyone who has ever asked me “but why is the bill like that,” which is the question this whole page is an answer to. Mistakes, rounding and simplifying assumptions are mine; the prices are Anthropic’s published list rates and will drift.

Read nextThe shape of a token bill