Prompt caching, hands-on
A hands-on cookbook for prompt caching: put a cache_control breakpoint at the end of a stable prefix, reuse the computed state on later calls, and read the cache_creation / cache_read usage fields to prove it worked — for a large cost and latency win. Real Anthropic SDK code throughout, plus one offline calculator you can run right now.
Learning objectives
- Explain what prompt caching is and why it cuts both cost and latency.
- Place a
cache_controlbreakpoint at the end of the stable prefix. - Read
cache_creation_input_tokensandcache_read_input_tokensfrom usage. - Reason about the ~5-minute TTL and what invalidates a cache entry.
- Use multiple breakpoints and do the cache write/read cost math.
- Design a production caching strategy that actually gets hits.
1 · What prompt caching is essential
Here's a bill that surprises everyone the first time. You build a support agent with a 20,000-token system prompt — a company handbook pasted into the context so the model always has the policies to hand. It works beautifully. Then the invoice arrives, and you realize you paid to process those same 20,000 tokens on every single request, thousands of times a day, even though not one byte of the handbook ever changed. You were charged full price, over and over, to re-read a document the model had already seen a second ago. Prompt caching is the fix — and once you see the waste, you can't unsee it.
The reason this happens is that a language model has no memory between API calls. Each request is a fresh, stateless event: the model wakes up knowing nothing, reads your entire prompt from the first token to the last, and only then starts answering. There's no "oh, you again — I still have your handbook loaded"; there is just a giant block of text arriving cold. The expensive first phase of that — where the model chews through all your input to build up its internal state before it writes a single word of reply — is called prefill, and prefill cost scales directly with how many input tokens you send. A big fixed prefix means a big fixed prefill bill, paid in full every time.
The tangible analogy: imagine a brilliant consultant who, by contract, must re-read the entire 200-page policy binder from scratch at the start of every meeting before answering your one new question. Genius answers — absurd process. What you obviously want is for them to read the binder once, keep it fresh in their head, and on the next meeting just field the new question. Prompt caching gives the API exactly that: process the stable material once, remember the computed state, and on later calls skip straight to the new part.
The common mistake is treating this as a discount you bolt on at the end without changing how you build the prompt. It isn't — it's an architectural choice about ordering. Caching only helps the part of your prompt that stays byte-for-byte identical across calls, and only the part that comes before the changing content. If your stable handbook sits after this-request's question, there is no reusable prefix and nothing to cache. So the real skill here isn't a magic parameter — it's learning to assemble prompts as "big stable thing first, small changing thing last," which is exactly the shape the rest of this page drills into.
Every time you call the model, the whole prompt — your system prompt, tool definitions, and the conversation so far — is re-read and re-processed from scratch. If a big chunk of that prompt is identical from one call to the next (a long system prompt, a big document you keep asking about, a fixed list of tool definitions), you are paying to process the same tokens over and over.
Prompt caching lets the API remember the computed state of a stable prefix and reuse it on the next call. The first call writes the cache; later calls with the same prefix read it — skipping the re-computation. You mark where the stable part ends with a cache breakpoint (cache_control), and the API caches everything up to and including that point.
This picture is the whole idea of prompt caching in one row: split your prompt into a stable prefix and a variable suffix, and mark the boundary between them.
- The green box (Stable prefix) is the part that's identical call after call — your system prompt, a big document, your tool definitions.
- The amber box ([cache breakpoint]) is the
cache_controlmarker. Everything up to and including it is what gets cached. - The blue box (Variable suffix) is what changes each call — this request's question. It sits after the breakpoint, so it never gets cached (and never invalidates the cache).
- The caption's rule: the first call computes and stores the green part (a cache write); later calls with the same green bytes reuse it (a cache read) and only process the blue part fresh.
In short: Stable stuff first, changing stuff last, breakpoint on the boundary. Get the order wrong — a timestamp in the green box — and there's no reusable prefix to cache.
The rule that governs everything: caching is a prefix match. The cache key is the exact bytes up to the breakpoint. On the first call that prefix is computed and stored (a cache write). On later calls with a byte-identical prefix, the stored state is reused (a cache read) and only the variable suffix after the breakpoint is processed fresh.
2 · Why it saves cost and latency essential
Most people reach for caching to save money, then discover the latency win is what their users actually feel. Two separate bills come due on every input token: you pay in dollars (input tokens are billed) and you pay in time (the model has to prefill those tokens before it can start replying). A cached prefix cuts both at once — and if you serve a chat UI, the time cut is the one people notice, because it's the difference between a reply that starts typing instantly and one that stalls for two seconds while the model re-reads a document it saw moments ago.
Think of it like a compiler's build cache. Compiling from scratch is slow and burns CPU; a warm build cache reuses the work it already did and finishes in a fraction of the time. Prompt caching is the same trade for a language model: the "compiled" internal state of your stable prefix is stored, and later calls reload it instead of rebuilding it. The reload isn't quite free — there's a lookup and a small serving cost — but it's a tiny fraction of doing the whole prefill over.
The mistake to guard against is thinking the discount is automatic and enormous on every call. It isn't: the very first call is more expensive, not less, because someone has to pay to build the cache in the first place. Caching is an investment that pays back only on reuse — the win comes from the second call onward, and it compounds the more times you hit the same warm prefix. Which is why the two numbers below — the write premium and the read discount — are the whole economic story, and section 5 turns them into arithmetic you can run.
Read it left to right as successive calls that all send the same stable prefix; the bar height is what that prefix costs on that call, measured against the normal input price.
- The dashed 1× line is the baseline: without caching, every call pays this full price for the prefix — a flat row of full-height bars.
- The tall red bar on call 1 is the cache write at ~1.25× — slightly above the 1× line, because populating the cache costs a premium.
- The thin green bars on calls 2, 3, … are cache reads at ~0.1× — a tenth of the baseline. The tall vertical gap between each green bar and the dashed line is the money (and prefill time) you save that call.
- The payoff: one small over-payment up front buys a near-flat, near-free cost on every call after — which is why the win compounds with reuse.
In short: One call is a loss (1.25× > 1×); from the second call on, each read at 0.1× sits far below the 1× line, and the savings pile up the more you reuse the prefix.
Reusing the computed prefix is cheaper on both axes. On cost: a cache read is billed at roughly 0.1× the normal input price for those tokens, versus the full 1× you'd otherwise pay every call. On latency: the model skips the prefill work for the cached tokens, so time-to-first-token drops — often dramatically when the prefix is large (tens of thousands of tokens of system prompt or document).
The catch is the write: the first call pays a small premium — about 1.25× for the default 5-minute cache (or 2× for the 1-hour TTL) — to store the prefix. So caching pays off once you reuse the prefix enough to amortize that write. With the 5-minute cache, that's just two calls.
3 · The request shape — cache_control on a content block essential
This is the section where "caching" stops being a concept and becomes one line of JSON. After all the theory, the actual mechanism is almost anticlimactic: you attach a small marker — "cache_control": {"type": "ephemeral"} — to a block of your prompt, and everything up to and including that block becomes cacheable. There is no separate "cache API," no key you manage, no store to configure. You just point at the end of your stable material and say "cache up to here."
The mental model that makes this click: think of it like a bookmark, not a highlighter. You aren't selecting a region to cache — you're dropping a single marker that says "the stable prefix ends here." The API caches from the very start of the prompt up to that bookmark, and treats everything after it as fresh, variable content. That's why placement is everything: the marker's job is to sit exactly on the boundary between "never changes" and "changes every call."
One detail trips up nearly everyone, and it's worth pausing on: to attach cache_control, the system field (or a message's content) has to be a list of content blocks, not a plain string. A bare string has nowhere to hang the marker. So the very first change caching forces on your code is turning system="..." into system=[{"type": "text", "text": "...", "cache_control": {...}}]. And the ordering rule underneath it is fixed and non-negotiable: the API renders your request as tools → system → messages. Because a breakpoint caches everything before it, dropping one marker on the last system block conveniently captures your tool definitions and your whole system prompt together — the two things most likely to be stable — in a single stroke.
cache_control on the last system block caches the tools and the whole system prompt together; the user message, rendered after the dashed boundary, stays variable and uncached.
Read left to right in the fixed render order the API uses: tools, then system, then messages. The dashed vertical line is the cache boundary set by your one marker.
- The two green boxes (tools, system) are the stable prefix — the same bytes call after call.
- The marker lives on the last block of
system. Because a breakpoint caches everything before and including it, it sweeps up the tools too — even though you only marked a system block. - The blue box (messages) sits to the right of the dashed boundary: it is the variable suffix, processed fresh every call and never cached.
- The payoff: you drop one bookmark on the boundary and get both of your most-stable pieces — tools and system — cached in a single stroke.
In short: Marker on the last system block = tools + system cached; the question after it stays fresh. Placement is the whole trick.
You cache by putting "cache_control": {"type": "ephemeral"} on the last content block of the stable prefix. Render order is tools → system → messages, so a breakpoint on the last system block caches the tools and the system prompt together. The block below is real Anthropic SDK — it needs a key and a network connection to run.
cache_system.py# needs: pip install anthropic + ANTHROPIC_API_KEY
# ▶ needs API key + network — this really calls the API; it will NOT run offline.
import anthropic
client = anthropic.Anthropic()
LARGE_SYSTEM = open("company_handbook.txt").read() # e.g. tens of thousands of tokens
resp = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
system=[
{
"type": "text",
"text": LARGE_SYSTEM,
# Breakpoint at the END of the stable prefix. Everything up to and
# including this block is cached; the user message after it is not.
"cache_control": {"type": "ephemeral"},
}
],
messages=[{"role": "user", "content": "Summarize the PTO policy in 3 bullets."}],
)
print(resp.content[0].text)
This is the minimal caching recipe: take one big, unchanging system prompt and cache it so the next call doesn't re-process it. It's real Anthropic SDK — it needs a key and a network, so it will not run offline; read it for the shape.
client = anthropic.Anthropic()builds the client; it picks upANTHROPIC_API_KEYfrom the environment automatically.systemis a list of content blocks (not a plain string) so a single block can carry"cache_control": {"type": "ephemeral"}. That marker is the cache breakpoint.- Because render order is tools → system → messages, the breakpoint on this last system block caches the tools and the whole system prompt together — everything before the user message.
- The user message comes after the breakpoint, so it's the variable suffix: it changes per request and is never cached.
What the output means: Nothing prints about caching here — this call just answers. You confirm the cache worked by reading resp.usage, which the next recipe does.
Try this: Mentally move the cache_control onto the user message instead. Now the breakpoint is after the changing question, so every call is a distinct prefix and nothing is ever reused — the classic mistake.
cache_control={"type": "ephemeral"} as a top-level argument to messages.create(...) and the API auto-places the breakpoint on the last cacheable block. Use the per-block form when you want to control exactly where the prefix ends.4 · Reading the usage fields — proof of a hit intermediate
Here's the trap that catches confident engineers: caching fails silently. When it doesn't work, nothing breaks. No error, no warning, no exception — your agent answers exactly as before. The only symptom is a bill that's quietly 10× higher than you assumed, discovered weeks later when someone finally reads the invoice. A stray timestamp in your system prompt can turn every "cached" call into a full-price cold write, and your code will never say a word about it. That's why this section isn't optional polish — it's the safety check that tells you whether the thing you built is actually saving money or just looking like it should.
The good news is that the API hands you a receipt on every single response. Buried in the usage object are three token counters that add up to a complete story of what just happened: how many tokens were written to the cache, how many were read from it, and how many were processed fresh at full price. Reading them is like checking the itemized receipt after a shop tells you a discount was applied — you don't take their word for it, you look for the line that says the discount actually came off.
The tell you're hunting for is a simple flip between the first call and the second. On a cold prefix, the write counter is large and the read counter is zero — you paid to populate the cache. On the very next call with a byte-identical prefix, they swap: the write counter drops to zero and the read counter jumps up to (roughly) the same number. That flip — write on call one, read on call two — is the fingerprint of caching working. The common mistake is skipping this check entirely and assuming the marker "did its thing"; the second most common is seeing the read counter stubbornly stuck at zero and blaming the API, when the real cause is almost always something in your own prefix changing between the two calls.
cache_creation is large and cache_read is zero; on the second identical call they swap — that flip is your proof the cache was reused.
Read the two columns side by side: the left is the first (cold) call, the right is the second call with a byte-identical prefix. Each column lists the three token counters from the usage object.
- Top row is
cache_creation_input_tokens— tokens written to the cache. It is a big red bar on call 1 (you paid to populate it) and 0 on call 2. - Middle row is
cache_read_input_tokens— tokens served from cache. It is 0 on call 1 and a big green bar on call 2. - Bottom row is
input_tokens— the fresh, full-price suffix. It is the same small blue bar in both columns, because the question after the breakpoint is always processed fresh. - The payoff element is the swap across the dashed line: creation→0, read→big. If that flip doesn't happen on call 2, the cache isn't being reused.
In short: Call 1 writes (creation big, read 0); call 2 reads (creation 0, read big). Watch those two numbers trade places — that is caching working, in receipts.
Never assume caching worked — measure it. Every response carries a usage object with three token fields that together tell the whole story:
| Field | Meaning | Billed at |
|---|---|---|
cache_creation_input_tokens | tokens written to cache this call | ~1.25× (write premium) |
cache_read_input_tokens | tokens served from cache this call | ~0.1× (cheap) |
input_tokens | uncached tokens processed fresh | 1× (full price) |
On the first call you expect a big cache_creation_input_tokens and a zero cache_read_input_tokens. On the second identical-prefix call they flip: cache_creation drops to 0 and cache_read jumps up. The block below reads those fields — real SDK, needs a key and network.
read_usage.py# needs: pip install anthropic + ANTHROPIC_API_KEY
# ▶ needs API key + network — real API calls; will NOT run offline.
import anthropic
client = anthropic.Anthropic()
system = [{
"type": "text",
"text": open("company_handbook.txt").read(),
"cache_control": {"type": "ephemeral"},
}]
def ask(question):
resp = client.messages.create(
model="claude-opus-4-8",
max_tokens=256,
system=system, # byte-identical prefix each call
messages=[{"role": "user", "content": question}],
)
u = resp.usage
print(f"created={u.cache_creation_input_tokens} "
f"read={u.cache_read_input_tokens} "
f"fresh={u.input_tokens}")
return resp
ask("Summarize the PTO policy.") # 1st: created=<big> read=0
ask("What about parental leave?") # 2nd: created=0 read=<big>
Caching is invisible unless you measure it. This recipe calls twice with the same big prefix and prints the three usage fields so you can watch the cache go from a write to a read. Real SDK — needs a key and network.
ask()sends the same cachedsystemprefix every time; only thequestionafter the breakpoint changes.u.cache_creation_input_tokenscounts tokens written to the cache (you pay the ~1.25× premium on these).u.cache_read_input_tokenscounts tokens served from cache (~0.1×).u.input_tokensis the uncached remainder at full price.- The first
ask()writes the prefix:createdis large,readis 0. The second flips them:created=0,readis large — that's your proof of a cache hit.
What the output means: Two lines of created=… read=… fresh=…. The jump in read from 0 to a big number on the second call is the whole point — that's the cache being reused.
Try this: If read stays 0 on the second call, something in the prefix changed between calls. Diff the two system strings byte-for-byte — a stray timestamp or unsorted JSON is the usual cause.
datetime.now() or a UUID in the system prompt, a json.dumps() without sort_keys=True, or a tool list that changes between calls. Any of these changes the prefix bytes, so every call writes a fresh entry and nothing is ever read.5 · The cost math (runs offline) intermediate
Before you wire up a single API call, you can answer "is caching even worth it here?" with grade-school arithmetic. That matters because caching is not free and not always a win — and the failure mode is subtle. Cache a prefix you only ever use once and you've made things worse: you paid the write premium for a discount you never collected. The whole decision turns on one question — how many times will this exact prefix be reused? — and that's a number you can estimate on a napkin.
The analogy is a warehouse-club membership. You pay an up-front fee (the write premium) to unlock a lower per-item price (the cheap reads). One visit and the membership was a waste — you'd have been better off paying retail. But shop there regularly and the fee amortizes to nothing while the per-item savings pile up. Prompt caching has exactly this shape: a one-time cost you're betting you'll earn back, and then some, through repetition.
Only two multipliers drive the whole model, and they're the ones from section 2: a write costs about 1.25× a normal input token (the membership fee), and a read costs about 0.1× (the members' price). Everything else is counting. Uncached, N calls pay full price N times. Cached, you pay 1.25× once and 0.1× on the other N−1 calls. The mistake to avoid is reasoning about this in your head at scale — the intuition that "caching saves ~90%" is only true in the limit of heavy reuse, and wildly wrong at low reuse where the fixed write premium dominates. The tiny function below makes the crossover concrete: it's pure standard library, runs with a plain python cache_savings.py, and prints both the steady-state savings and the exact break-even point where caching stops being a loss and starts being a win.
The horizontal axis is the number of calls made against the same prefix; the vertical axis is the running total cost. Two lines climb across it — compare their slopes.
- The steeper grey line (uncached) rises by a full 1× with every call — no discount is ever applied.
- The flatter blue line (cached) starts a hair higher than the grey one at call 1 (you paid the ~1.25× write premium) but then climbs by only ~0.1× per call.
- The amber dot marks the crossover: because the cached line rises so much more slowly, it dips below the uncached line by about the second call — the break-even point.
- The payoff is the ever-widening vertical gap after the crossover: every additional reuse of the prefix makes caching cheaper by another ~0.9×.
In short: Caching starts as a small loss and turns into a win by the second call; the more you reuse the prefix, the bigger the gap between the two lines.
You can decide whether caching is worth it with arithmetic, no API needed. Without caching, N calls each pay full price (1×) for the prefix. With caching, the first call pays the write premium (~1.25×) and the other N−1 calls pay the read rate (~0.1×). The helper below is pure stdlib — it runs with a plain python file.py — and reports the tokens saved as a percentage.
cache_savings.pydef cache_savings(prefix_tokens, calls, write_mult=1.25, read_mult=0.1):
"""Compare paying full price N times vs. write-once + read (N-1) times.
Returns (no_cache_units, with_cache_units, percent_saved) in token-equivalents.
"""
no_cache = prefix_tokens * calls
with_cache = prefix_tokens * write_mult + prefix_tokens * read_mult * (calls - 1)
saved_pct = round(100 * (no_cache - with_cache) / no_cache)
return round(no_cache), round(with_cache), saved_pct
no_cache, with_cache, saved = cache_savings(prefix_tokens=10_000, calls=100)
print(f"no cache: {no_cache} token-units")
print(f"with cache: {with_cache} token-units")
print(f"saved: {saved}% on the cached prefix")
# Break-even: at how few calls does caching already win?
one, two = cache_savings(10_000, 1)[1], cache_savings(10_000, 2)[1]
print(f"1 call costs {one} (write premium, a loss); 2 calls cost {two} (already a win)")
no cache: 1000000 token-units
with cache: 111500 token-units
saved: 89% on the cached prefix
1 call costs 12500 (write premium, a loss); 2 calls cost 13500 (already a win)
Before you wire up caching, this offline calculator tells you whether it's even worth it. It's pure stdlib — it runs with a plain python cache_savings.py, no key needed — and turns the pricing multipliers into a savings percentage.
no_cache = prefix_tokens * callsis the naive cost: every one ofcallsrequests pays full price (1×) for the whole prefix.with_cachepays the write premium once (prefix_tokens * 1.25) and the cheap read rate on the other N−1 calls (prefix_tokens * 0.1 * (calls - 1)).saved_pctis how much of the prefix cost caching removes. At 100 reuses of a 10K-token prefix, that's ~89%.- The last line probes the break-even: 1 call is a small loss (you paid 1.25× for a prefix you used once); by 2 calls caching is already cheaper than not caching.
What the output means: no cache: 1000000 vs with cache: 111500 token-units — a saved: 89%. The final line shows the 1-call loss (12500) turning into a 2-call win (13500 < the 20000 two full calls would cost).
Try this: Call cache_savings(20_000, 5) and cache_savings(20_000, 100) and compare the percentages — the savings climb fast with reuse, then flatten as the one-time write premium gets amortized away.
Two lessons from the numbers. First, at 100 reuses of a 10K-token prefix you save ~89% of the prefix cost. Second, a single call costs 1.25× — caching a prefix you'll use only once is a small loss. It turns a profit at the second call and climbs from there.
6 · TTL and what invalidates the cache advanced
You've proven a cache hit and done the math — now here's what silently takes the hit away again. Two forces expire a cache entry, and confusing them is the source of most "it worked yesterday" caching bugs. The first is time: an entry doesn't live forever, it decays. The second is change: alter the prefix by a single byte and the entry you had is instantly worthless. Understanding both is what separates a cache that quietly delivers savings for months from one that "works in the demo" and mysteriously stops.
Take time first. The word ephemeral in {"type": "ephemeral"} is a literal promise: the entry is short-lived. But the clock works like a car's motion-sensor light, not an egg timer — every time someone hits the cache, the countdown resets to full. So a steady drip of traffic keeps the prefix warm forever, while a quiet gap longer than the window lets it lapse and the next caller pays to rebuild it. That's the mental model for choosing a TTL: match the window to how often you expect the prefix to be touched, and remember that a longer window buys survival across idle gaps at the cost of a steeper write premium.
Now change — and this is where the prefix-match rule from section 1 comes back to bite. Because the cache key is the exact bytes up to the breakpoint, invalidation is total and unforgiving: one altered byte anywhere before the marker doesn't degrade the cache, it destroys the entire cached prefix after that point. The analogy is a book index built for one specific edition — reflow the text by inserting a single word on page one and every page number in the index is wrong; the index isn't "mostly right," it's useless. The mistake that burns people is thinking of invalidators as "the prompt" only. They're anything that changes those bytes — and that includes things that don't feel like prompt content at all: a tool list rebuilt in a different order, a model swap, a json.dumps() whose keys came out in a different order this time, an iterated set. The list below names the usual suspects so you can hunt them by category rather than one painful surprise at a time.
Follow the arrows as a lifecycle: an entry moves between three states — Cold, Warm, and Expired — driven by what your traffic does.
- The red "Cold" box is the starting point: no usable entry. The first call pays the ~1.25× write to move it to Warm.
- The green "Warm" box is a live entry. The looping arrow on top is the key behaviour: each hit reads at ~0.1× and resets the ~5-minute TTL, so steady traffic keeps it warm indefinitely.
- The amber arrow to "Expired" fires on either trigger — a gap longer than the TTL, or any byte change in the prefix — and from there the next call is a cold write again.
- The payoff: two independent forces end a cache — time (idle gap) and change (edited bytes). Both send you back to the expensive Cold write.
In short: Write → Warm; every hit resets the timer and reads cheap; an idle gap or an edited byte expires it back to a cold write.
A cache entry is ephemeral: by default it lives about 5 minutes, and the clock resets on every hit. So a steady stream of requests keeps the prefix warm indefinitely; a 6-minute gap lets it expire and the next call pays the write again. For bursty traffic with long gaps you can request a 1-hour TTL with {"type": "ephemeral", "ttl": "1h"} — but the write premium doubles to ~2×, so it needs more reads to pay off.
Invalidation follows straight from the prefix-match rule: any byte change anywhere before the breakpoint invalidates everything after it. That includes changes you might not think of as "the prompt":
What invalidates a cache entry
- Editing the prefix text — a changed system prompt, a re-ordered document, a stray whitespace edit.
- Changing the tool set — adding, removing, or re-ordering tools. Tools render at position 0, so this invalidates everything.
- Switching models — caches are per-model; the same prefix on a different model is a cold write.
- Non-deterministic serialization —
json.dumps()without sorted keys, iterating aset, or an interpolated timestamp/UUID. - Letting it expire — no hit within the TTL window and the entry is gone.
7 · Multiple breakpoints & multi-turn caching professional
A single breakpoint is fine for a one-shot call, but a real agent holds a conversation — and that's where one marker isn't enough. The problem: in a chat, some of your prompt never changes (the system prompt, the tools) while another part grows a little with every turn (the conversation history). A single breakpoint forces you to pick one boundary, but you actually have two things worth caching that move at completely different speeds. This section is about caching both at once.
The mental model is geological strata, or tree rings. Your prompt has layers of different stability: a bedrock layer that's frozen for the life of the app (system + tools), and above it a layer that accretes one thin band per turn (each new user/assistant exchange). You want a cache marker at the top of each stable layer, so the bedrock is read once and reused forever while each new band gets written once and then read cheaply on every turn after. The API gives you up to 4 breakpoints per request precisely so you can mark several stability boundaries like this.
The move that makes multi-turn caching pay off is walking the breakpoint forward: each turn, you place a marker on the last block of the newest message. That freezes everything said so far as a cacheable prefix, so the next turn reads the entire prior conversation from cache and writes only its own new slice. Caching accrues incrementally — the read total climbs turn by turn while the fresh cost stays a thin sliver. The mistake that quietly kills this is rebuilding the stable layer slightly differently in a background job — a summarizer or sub-agent that reassembles system, tools, or model even a hair differently from the main loop misses the shared entry completely and pays cold writes. The rule: copy the parent's stable fields verbatim and append fork-specific content strictly after the breakpoint.
Read the stacked bands top to bottom as layers of your prompt, ordered from most stable (top) to most volatile (bottom) — like geological strata.
- The top green band (system + tools) is bedrock: frozen for the life of the app. Breakpoint #1 sits at its edge, so it's written once and read on every future turn.
- The middle green band (conversation so far) accretes one thin slice per turn. Breakpoint #2 sits on its trailing edge and walks forward each turn (the arrow), freezing everything said so far as a cacheable prefix.
- The bottom blue band (newest question) has no marker — it falls after #2 and is always processed fresh.
- The payoff: because #1 and earlier positions of #2 stay valid read points, each new turn reads the entire prior prefix from cache and pays full price only on its own new slice — savings accrue turn by turn.
In short: Mark each stability layer separately (up to 4 markers); the frozen layer is read forever, the conversation layer's breakpoint walks forward, and only the newest question is fresh.
You get up to 4 breakpoints per request, which lets you cache at multiple stability boundaries. A common layout: one breakpoint after the tool definitions + frozen system prompt (never changes), and a second on the last block of the most recent conversation turn (grows as the chat continues). Each turn re-reads the entire prior prefix cheaply and only writes the newest slice. The block below shows both — real SDK, needs a key and network.
multi_breakpoint.py# needs: pip install anthropic + ANTHROPIC_API_KEY
# ▶ needs API key + network — real API call; will NOT run offline.
import anthropic
client = anthropic.Anthropic()
# Breakpoint #1: the frozen system prompt (+ any tools) — cached once, reused forever.
system = [{
"type": "text",
"text": open("company_handbook.txt").read(),
"cache_control": {"type": "ephemeral"},
}]
# Breakpoint #2: the last block of the newest turn — the conversation prefix grows,
# so each turn reads everything before it cheaply and writes only the new slice.
messages = [
{"role": "user", "content": "What's our refund window?"},
{"role": "assistant", "content": "30 days from delivery."},
{
"role": "user",
"content": [
{
"type": "text",
"text": "And for digital goods?",
"cache_control": {"type": "ephemeral"},
}
],
},
]
resp = client.messages.create(
model="claude-opus-4-8", max_tokens=256, system=system, messages=messages,
)
u = resp.usage
print(f"read={u.cache_read_input_tokens} created={u.cache_creation_input_tokens}")
This recipe caches at two stability boundaries at once — a frozen prefix that never changes and a conversation that grows each turn. Real SDK; needs a key and network.
- Breakpoint #1 sits on the
systemblock: the handbook + any tools. It's written once and read on every future turn, forever. - Breakpoint #2 sits on the last block of the newest user turn. As the chat grows, each turn reads the entire prior conversation cheaply and writes only the newest slice — caching accrues incrementally.
- Note the newest user turn is a list of content blocks (not a bare string) so it can carry its own
cache_control. The earlier turns are plain strings — they don't need a marker, they're read via breakpoint #2's prefix. - You get up to 4 breakpoints per request; here two is enough — one for the never-changes prefix, one for the grows-each-turn conversation.
What the output means: read=… created=…: after the first turn, later turns show a large read (the frozen system + prior turns) and a small created (just the newest turn).
Try this: Add another user/assistant turn and move breakpoint #2 onto the new last block. The previous turn's tokens shift from created into read — the conversation prefix is now cached up to the new boundary.
A subtle production detail: caches are scoped by TTL, model, and prefix bytes. If a background job (summarization, a sub-agent) rebuilds system/tools/model even slightly differently from the main loop, it misses the main cache entirely. Copy the parent's stable fields verbatim and append fork-specific content after the breakpoint.
8 · Tech-lead — a caching strategy that gets hits tech-lead
The gap between "we use caching" and "caching actually saves us money" is where most teams quietly lose the win. An individual engineer places a cache_control marker, sees a read on their test call, and moves on. Then the codebase evolves: someone adds a per-user greeting to the system prompt, a refactor reorders the tool list, a new feature interpolates a request ID "just for logging." Each change is reasonable in isolation, and each silently zeroes the hit rate. Six months later the cache is decorative — every marker still there, every call a cold write. A lead's job is to make sure that decay never happens invisibly.
The reframe that separates a lead from a contributor is treating hit rate as a monitored metric, not a hopeful annotation. The analogy is a cache in any other system — a CDN, a database query cache: no serious team ships one and simply assumes it's working. They graph the hit rate, they alert when it drops, they treat a falling hit rate as an incident because it maps directly to a cost regression. Prompt caching deserves the same discipline. The single most useful signal is production cache_read_input_tokens: watch it in aggregate, and the moment someone lands a change that poisons the prefix, reads collapse toward zero and your dashboard tells you before the invoice does.
The mistake that defines this level is optimizing the marker placement while leaving the prompt assembly undisciplined. Caching is a property of how you build the request, so the design has to guarantee stability by construction: a frozen prefix (system + deterministically serialized, sorted tool definitions), all per-request content pushed strictly after the last breakpoint, a written breakpoint plan for which of the 4 slots caches what and why, and a TTL matched to traffic shape. Own those four things — the frozen contract, the breakpoint plan, the TTL choice, and the hit-rate alarm — and a large, repetitive prompt surface stays both fast and affordable as the system grows. Skip any one and the cache erodes the first time the code changes hands.
A lead owns the cache hit rate as a real, monitored metric — not a hopeful cache_control sprinkled on and forgotten. That means designing the prompt assembly so the stable content is genuinely stable, and instrumenting cache_read_input_tokens in production to catch regressions the moment someone interpolates a timestamp into the system prompt.
The contract to own: a frozen prefix (system + deterministic, sorted tool definitions) with dynamic context pushed after the last breakpoint; a breakpoint plan (which of the 4 slots caches what, and why); a TTL choice matched to traffic shape (5-minute for steady load, 1-hour for bursty); and a hit-rate dashboard that alerts when reads fall toward zero. Get those right and a large, repetitive prompt surface stays both fast and affordable at scale.
Read left to right: four inputs a lead owns on the left, all converging on the single outcome on the right.
- The four left boxes are the responsibilities: a frozen prefix contract (system + deterministically sorted tools), a breakpoint plan (which of the 4 slots caches what and why), a TTL choice matched to traffic shape, and a hit-rate alarm on production
cache_read_input_tokens. - The arrows show every one of them feeding the result — this is an AND, not a menu: all four are required.
- The green box (high, stable hit rate) is the payoff — a large, repetitive prompt surface that stays both fast and affordable as the system grows.
- The failure mode the caption names: skip any single input and the hit rate quietly decays the next time someone edits the prompt.
In short: Frozen contract + breakpoint plan + right TTL + hit-rate alarm → a hit rate you can trust; miss one and the cache silently erodes.
cache_read_input_tokens > 0 across repeated requests — before you rely on the cache_savings.py numbers. A cache you think is working but isn't is worse than none: you pay the 1.25× write on every single call and never collect the read discount.Exercise AP2.1 — Prove a hit, then a miss
Context: Seeing a cache hit collapse to a miss the instant you inject volatile content is the fastest way to internalize the prefix-match rule. Proving it on a real call beats reading about it.
Your task: Using read_usage.py, call twice with a byte-identical large system prefix to confirm a cache read, then deliberately break it by interpolating datetime.now() into the system text and re-run.
Requirements:
- Needs a real API key
- The second identical call shows
cache_read_input_tokens> 0 - After injecting
datetime.now()into the system text,cache_readcollapses to 0 - Explain which caching rule the timestamp violated
💡 Hint: The break should live in the system prefix, not the user message — that is what makes the prefix differ and kills the read.
Exercise AP2.2 — Do the break-even by hand, then check it
Context: Caching pays off only past a reuse threshold, so a prefix touched once per session may not be worth it. Working the numbers by hand and checking them against the script builds the intuition.
Your task: Use cache_savings.py to find the percentage saved at 5, 20, and 100 calls for a 20,000-token prefix, then decide whether caching a once-per-session prefix is worth it.
Requirements:
- Runs offline — no API key needed
- Report the percentage saved at 5, 20, and 100 calls
- Decide whether a prefix used exactly once per session is worth caching
- Back the decision with the 1-call versus 2-call numbers the script prints
💡 Hint: Compare the single-use write cost (1.25x) against paying full price once — one read is never enough to recover a write.
🪜 Practice ladder beginner → industry
Six graded exercises, easy to real-world. Try each before opening its solution.
Context: Prompt caching lets a large, unchanging prefix be reused across requests instead of re-billed in full every time. The whole mechanism turns on placing one breakpoint correctly and reading two usage fields to confirm it worked.
Your task: Cache a large shared system prompt with cache_control on the last system block, and name the two usage fields that distinguish a write from a hit.
Requirements:
- Needs a real API key to run
- The breakpoint is
cache_control: {"type": "ephemeral"}on the last block of the stable prefix - The first call shows
cache_creation_input_tokens> 0 (write) - A later byte-identical call shows
cache_read_input_tokens> 0 (read) - A read stuck at 0 means the prefix is not actually identical
💡 Hint: Put the big shared context in a system block, mark that block ephemeral, and vary only the user message between the two calls.
Show solution
The breakpoint goes on the last block of the stable prefix:
import anthropic
client = anthropic.Anthropic()
resp = client.messages.create(
model="claude-opus-4-8", max_tokens=512,
system=[{
"type": "text",
"text": LARGE_SHARED_CONTEXT, # e.g. a 20k-token manual
"cache_control": {"type": "ephemeral"}, # 5-minute TTL by default
}],
messages=[{"role": "user", "content": "Summarize section 3."}],
)
print(resp.usage.cache_creation_input_tokens) # >0 on the FIRST call (cache write)
print(resp.usage.cache_read_input_tokens) # >0 on LATER calls (cache read)
First call writes the cache (cache_creation_input_tokens); subsequent calls with a byte-identical prefix read it (cache_read_input_tokens). If the read stays 0, something in the prefix is changing.
Context: Caching is not free: the write costs more than a normal read, so it only pays off past a break-even number of reuses. Modeling that economics keeps you from caching a prefix that is used only once.
Your task: Write cache_savings(prefix_tokens, n_requests) comparing cached versus uncached total input cost, and show it for a 20k-token prefix over 10 requests.
Requirements:
- Pure Python — runs offline, no API key
- Uncached bills the full prefix at base input price on every request
- Cached bills the first request at 1.25x (5-minute TTL) and each later one at 0.1x
- Report the percentage saved for the 20k/10 case
- Make clear that two requests already beat uncached (1.25 + 0.1 < 2)
💡 Hint: Split the cached cost into a one-time write term and a per-remaining-request read term; the more reads per write, the closer you get to the ~90% ceiling.
Show solution
Model the economics directly — pure Python:
IN = 5.0 / 1e6 # $ per token, Opus 4.8 input
def uncached(prefix, n):
return prefix * IN * n # full price every time
def cached(prefix, n):
write = prefix * IN * 1.25 # first request: 1.25x
reads = prefix * IN * 0.10 * (n - 1) # rest: 0.1x
return write + reads
p, n = 20_000, 10
print(f"uncached: ${uncached(p, n):.4f}") # $1.0000
print(f"cached : ${cached(p, n):.4f}") # $0.2150
print(f"saved : {100*(1-cached(p,n)/uncached(p,n)):.0f}%") # 78%
Two requests already beat uncached at 5-minute TTL (1.25 + 0.1 = 1.35 < 2). The more reads per write, the closer you get to the ~90% ceiling.
Context: Caching is a strict prefix match, so a single volatile byte near the front silently defeats it and every request quietly pays full price. Spotting this class of bug is a core caching skill.
Your task: Explain why a system prompt beginning with f"Today is {datetime.now()}. " never caches, and give the fix.
Requirements:
- Diagnose that a changing byte at position N invalidates everything at positions ≥ N
- Identify the leading timestamp as the reason the prefix is never repeated
- Fix by freezing the cached prefix and moving volatile content after the last breakpoint
- Generalize the rule to other volatile content: UUIDs, per-request ids, unsorted
json.dumps
💡 Hint: Anything that differs per request must live after the last cache_control block — move the date into the user message.
Show solution
Caching is a prefix match: one byte change at position N invalidates everything at positions ≥ N.
# BROKEN — the timestamp changes every request, so the prefix never repeats:
system = [{"type": "text",
"text": f"Today is {datetime.now()}. " + MANUAL,
"cache_control": {"type": "ephemeral"}}]
# FIXED — freeze the prefix; move volatile content AFTER the last breakpoint:
system = [{"type": "text", "text": MANUAL,
"cache_control": {"type": "ephemeral"}}]
messages = [{"role": "user",
"content": f"Today is {datetime.now()}. Summarize section 3."}]
The date sat at the front of the cached prefix, so every request had a unique prefix and paid full price. Anything volatile — timestamps, UUIDs, per-request ids, unsorted json.dumps — must live after the last cache_control breakpoint (or be deleted).
Context: In a growing agent conversation, you want each new turn to read the entire prior history from cache instead of re-billing it. Moving the breakpoint correctly each turn is what makes the read total grow over time.
Your task: In a multi-turn loop, place a breakpoint so every new turn reuses the whole prior conversation prefix, and explain why earlier breakpoints still count as reads.
Requirements:
- Needs a real API key to run
- Mark the last content block of the most recent message as the breakpoint each turn
- The reported
cache_read_input_tokensgrows as the conversation lengthens - Respect the max of 4 breakpoints per request; earlier ones remain valid read points
- Note the 20-block lookback limit for very long tool-heavy turns
💡 Hint: Set cache_control on the newest turn's last block each step; the system prompt keeps its own breakpoint so both are read.
Show solution
Cache the last content block of the most recent turn each request:
def step(messages):
# mark the last block of the last message as the cache breakpoint
last = messages[-1]
if isinstance(last["content"], list):
last["content"][-1]["cache_control"] = {"type": "ephemeral"}
resp = client.messages.create(
model="claude-opus-4-8", max_tokens=1024,
system=[{"type": "text", "text": SYSTEM,
"cache_control": {"type": "ephemeral"}}],
messages=messages,
)
print("read:", resp.usage.cache_read_input_tokens) # grows each turn
return resp
Max 4 breakpoints per request, but earlier ones stay valid read points — so as the conversation grows, each turn reads the whole prior prefix from cache and only the newest turn is full price. Watch the 20-block lookback: very long tool-heavy turns may need an intermediate breakpoint.
Context: The only reliable guarantee of cache reuse is a byte-identical prefix, so cache-safety reduces to one testable invariant. An automated audit catches the usual invalidators before they cost money in production.
Your task: Write audit(build_prompt) that renders two requests and flags a broken cache by diffing their stable prefixes, plus a checklist of usual suspects.
Requirements:
- Pure offline check — no API key required
- Render two requests where only the question differs
- Diff everything before the last breakpoint and require it to be identical
- Return a clear BROKEN vs OK verdict
- Call out the common invalidators:
datetime.now()/UUID in system, unsortedjson.dumps, per-user interpolation, tools rebuilt per request
💡 Hint: Serialize the stable prefix deterministically (e.g. json.dumps(..., sort_keys=True)) and compare the two renders.
Show solution
Prove cache-safety by rendering twice and diffing the prefix bytes:
import json
def render_prefix(req):
# tools -> system -> messages is the render order; hash the stable prefix
return json.dumps(req.get("system"), sort_keys=True)
def audit(build_prompt):
a = build_prompt(question="Q1")
b = build_prompt(question="Q2") # only the question should differ
if render_prefix(a) != render_prefix(b):
return "BROKEN: system prefix differs between requests -> no cache reads"
return "OK: prefix stable; volatile content is after the breakpoint"
# Usual suspects it catches: datetime.now()/uuid in system, unsorted json.dumps,
# per-user string interpolation in the prefix, tools reordered/rebuilt per user.
If the stable prefix is not byte-identical across requests, no marker helps. The audit reduces caching to one testable invariant: render two requests, diff everything before the last breakpoint, and demand they match.
Context: A multi-tenant RAG service has content at different stability tiers: a frozen global prompt, per-tenant docs, and per-request questions. Layering breakpoints by stability lets each tier cache at its own cadence.
Your task: Design what to cache across tenants — global system prompt, per-tenant docs, per-request question — choosing TTL and breakpoint placement, and defend the ordering.
Requirements:
- Order blocks stable→volatile: global first, tenant docs next, question last
- The frozen global prompt is a shared prefix reusable across all tenants
- Per-tenant docs get their own breakpoint for reuse within a tenant session
- The per-request question has no breakpoint and is always full price
- Justify a 1h TTL for bursty traffic (doubles write cost, survives idle gaps, needs ~3 reads to pay off)
- Keep both shared tiers before the question or cross-tenant sharing collapses
💡 Hint: Think of the prompt as concentric stability rings; a breakpoint after each shared ring lets the global tier be reused even across different tenants.
Show solution
Layer the prompt by stability so each tier caches at its own cadence (design + code):
def build(tenant_docs, question, tenant_id):
return dict(
model="claude-opus-4-8", max_tokens=1024,
system=[
# Tier 1: frozen, shared across ALL tenants -> global cache
{"type": "text", "text": GLOBAL_SYSTEM,
"cache_control": {"type": "ephemeral", "ttl": "1h"}},
# Tier 2: per-tenant retrieved docs -> per-tenant cache, bursty traffic
{"type": "text", "text": tenant_docs,
"cache_control": {"type": "ephemeral", "ttl": "1h"}},
],
# Tier 3: volatile per-request question -> NO breakpoint, always full price
messages=[{"role": "user", "content": question}],
)
Order stable→volatile: global first (shared prefix reused across tenants), tenant docs next (reused within a tenant session), question last (unique, uncached). Use 1h TTL for bursty tenant traffic — it doubles the write cost but survives idle gaps, so it needs ~3 reads to pay off; keep the two shared tiers before the per-request question or cross-tenant sharing collapses.
✓ Checkpoint — you can move on when you can…
- Explain prompt caching as a prefix match and where the breakpoint goes.
- Say why a cache read is cheaper (~0.1×) and faster than a fresh prefix.
- Read
cache_creation_input_tokens/cache_read_input_tokensto prove a hit. - List what invalidates a cache entry and why the system prompt must be frozen.
- Do the write (~1.25×) vs read (~0.1×) cost math and find the break-even.
- Design a multi-breakpoint, monitored caching strategy for production.
Knowledge check check yourself
How does the prefix-match rule determine a prompt-cache hit, and what happens if a single byte before the breakpoint changes?
Show answer
cache_control breakpoint (rendered as tools then system then messages). Any byte change anywhere before the breakpoint invalidates everything after it, forcing a cold write instead of a read, which is why the cached prefix must be frozen.Given a cache read costs ~0.1x and a write ~1.25x of normal input price, why does a single cached call lose money, and where is break-even?