Prompt caching is a 90% list-price discount on reused input tokens - but only while the prefix stays byte-identical across calls. A single interpolated timestamp, a reordered tool list, or a request ID near the top invalidates every cached token after it. You still get a response; you just pay full price plus the write premium, every call.
This playbook is the operational half of Stop paying full price for repeat LLM calls. Numbers and provider pricing live there; the checklist and verification snippet live here.
Definitions
Prompt prefix - the front portion of a request (system prompt, tool schemas, early history) that the provider matches byte-for-byte on a cache hit.
Cache marker - the boundary you set (Anthropic cache_control) or the provider sets (OpenAI auto-cache ≥1,024 tokens) between content eligible for reuse and content that changes every call.
Cache read - a later call that reuses a stored prefix at ~0.1× base input price (90% off on Anthropic and the GPT-5.x tiers covered in the cost article).
Placement rules
Three rules, grounded in the same PwC study that measured 41–80% field savings from caching (arXiv 2601.06007):
| Rule | Do | Don’t |
|---|---|---|
| Order | Stable content first, dynamic last | Put timestamps / user IDs above the marker |
| Scope | Cache system prompt + tool schemas | Cache dynamic tool results every turn |
| Reflex | Cache the stable prefix only | Cache the whole context by default |
Anthropic’s minimum cacheable prefix is model-dependent (1,024–4,096 tokens). Shorter prefixes silently don’t cache - no error, just zero reads. Cache time-to-live (TTL) also matters: a prefix you call once pays the write premium with no read to offset it.
Copy-paste: Anthropic stable prefix
from anthropic import Anthropic
client = Anthropic()
# STABLE - byte-identical across calls in this session
SYSTEM_PROMPT = """You are a ticket classifier.
Allowed labels: billing, outage, feature, other.
Reply with one label and a one-line reason.
"""
TOOLS = [
{
"name": "lookup_policy",
"description": "Fetch a frozen policy snippet by id",
"input_schema": {
"type": "object",
"properties": {"policy_id": {"type": "string"}},
"required": ["policy_id"],
},
}
] # freeze order; do not rebuild per request
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=64,
system=[
{
"type": "text",
"text": SYSTEM_PROMPT,
"cache_control": {"type": "ephemeral"},
}
],
tools=TOOLS, # keep schemas above any per-call noise
messages=[
{
"role": "user",
# DYNAMIC - after the cached block
"content": f"[{request_id}] {ticket_text}",
}
],
)
usage = response.usage
print(
usage.cache_creation_input_tokens,
usage.cache_read_input_tokens,
usage.input_tokens,
)
OpenAI caches automatically on prompts ≥1,024 tokens - same placement discipline applies even without a cache_control block.
Verify before you celebrate
# After N identical calls against the same prefix, reads should be > 0.
# Pseudo-check against your logged usage rows:
# cache_read_input_tokens == 0 on call 2+ → prefix is changing or too short
# cache_creation_input_tokens > 0 every call → you are rewriting the prefix
If reads stay at zero: diff the serialized request body between call 1 and call 2. Anything that changed above the marker is the bug. The garden rule Cache-stable prefix is the one-liner version of this section.
Stack, don’t substitute
Caching stacks with batch processing (95% combined on the shared prefix) and with context management on agent loops. It does not survive mid-session rewrites of cached content - that failure mode is covered in Trim output, not the cache. For tier routing instead of cache math, use the Claude tier guide.
Next move: apply the checklist on one high-volume endpoint, log cache_read_input_tokens for a day, then expand. Full lever map: Optimization.