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
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
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
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
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
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
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
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
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.
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?