Skip to content
ARTICLES
MenuClose
Optimization

Don't break the prompt cache

AUTHOR
Bartłomiej Krupa
PUBLISHED
2026.07.20
READ_TIME
6 min

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):

RuleDoDon’t
OrderStable content first, dynamic lastPut timestamps / user IDs above the marker
ScopeCache system prompt + tool schemasCache dynamic tool results every turn
ReflexCache the stable prefix onlyCache 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.

FAQ

Why is my prompt cache hit rate zero?
Almost always a byte change in the cached prefix - a timestamp, reordered tools, a request ID, or dynamic tool results sitting above the cache marker. Anthropic also silently skips prefixes shorter than the model minimum (1,024–4,096 tokens). Check usage.cache_read_input_tokens on repeated identical calls; zero means the prefix is changing or too short.
Where should dynamic content go in a cached prompt?
After the cache marker. Freeze the system prompt and tool schemas before it; put timestamps, user IDs, and per-request context after it. Caching dynamic tool results writes a new entry every turn instead of reading the old one.
Does naive full-context caching always help?
No. A January 2026 PwC study (arXiv 2601.06007) found that caching only the stable system prompt beats caching the whole context, and that naive full-context caching can increase latency. Cache the stable prefix; leave the rest uncached.