Skip to content

Browse catalog

Search catalog

[READY] Type a title, tag, or description.

Optimization

Don't break the prompt cache

AUTHOR
Bartłomiej Krupa
PUBLISHED
2026.07.20
UPDATED
2026.07.29
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, and the ordering is the opposite of the intuition - newer and pricier models have the lower floor:

Minimum prefixModels
512 tokensOpus 5, Fable 5
1,024 tokensOpus 4.8, Sonnet 5, Sonnet 4.6
2,048 tokensOpus 4.7
4,096 tokensOpus 4.6, Opus 4.5, Haiku 4.5

Shorter prefixes silently don’t cache - no error, just zero reads. The practical trap is routing: a 3,000-token prefix caches on Opus 5 and does not on Haiku 4.5, so the moment you fan work out to the cheap tier to save money, that tier starts paying full input price on every call. Check the floor for the model you route to, not the one you developed against. Every model’s floor, plus the invalidation hierarchy and the 20-block lookback window, is in Prompt cache floors by model. Cache time-to-live (TTL) also matters: a prefix you call once pays the write premium with no read to offset it.

You get four breakpoints, and they are a budget. Anthropic allows a maximum of 4 cache_control breakpoints per request, counted across tools + system + messages combined - and automatic caching consumes one of the slots. Exceed the limit and the request fails outright with a 400, which is the good outcome; the bad outcome is a long-running agent loop that keeps appending breakpoints as history grows and hits the ceiling mid-session.

Caching also matches on everything up to and including a marked block, in toolssystemmessages order. So spend the budget from the front: one breakpoint after tool schemas, one after the system prompt, and give a rolling message window whatever remains. An agent loop that marks each new turn is not caching more aggressively - it is running out of budget and re-writing the prefix.

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

Fire the same request twice and assert the second call reads the cache. Anything else is guessing. The garden note Cache-read verifier is this check in one place.

def check_cache(build_request, n=2):
    """Call the same request n times; the prefix is healthy if call 2+ reads."""
    for i in range(1, n + 1):
        u = client.messages.create(**build_request()).usage
        print(
            f"call {i}: write={u.cache_creation_input_tokens} "
            f"read={u.cache_read_input_tokens} uncached={u.input_tokens}"
        )
        if i > 1:
            assert u.cache_read_input_tokens > 0, "prefix changed or is under the minimum"
            assert u.cache_creation_input_tokens == 0, "prefix is being rewritten every call"


check_cache(lambda: dict(
    model="claude-sonnet-5",
    max_tokens=64,
    system=[{"type": "text", "text": SYSTEM_PROMPT,
             "cache_control": {"type": "ephemeral"}}],
    tools=TOOLS,
    messages=[{"role": "user", "content": "ping"}],
))

Healthy output writes on call 1 and reads on call 2:

call 1: write=1532 read=0    uncached=9
call 2: write=0    read=1532 uncached=9

When reads stay at zero, diff the two payloads instead of rereading the code - the byte that moved is rarely the one you suspect:

import json, difflib

a = json.dumps(build_request(), indent=2, sort_keys=True).splitlines()
b = json.dumps(build_request(), indent=2, sort_keys=True).splitlines()
print("\n".join(difflib.unified_diff(a, b, lineterm="")))

Building the request twice and diffing it catches the whole class in one shot: interpolated timestamps, a regenerated request ID, a tool list rebuilt from a dict in nondeterministic order, a session UUID in the system prompt. An empty diff with zero reads means the opposite problem - the prefix is stable but under the model’s minimum, so raise it or accept that this endpoint will not cache.

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, which is 512 tokens on Opus 5 and Fable 5, 1,024 on Opus 4.8 and Sonnet 5, and 4,096 on Haiku 4.5 - so the same prefix can cache on one model and silently not on a cheaper one. It also allows at most 4 cache_control breakpoints per request across tools, system, and messages combined. Check usage.cache_read_input_tokens on repeated identical calls; zero means the prefix is changing or too short. To find which byte moved, build the request twice and diff the serialized payloads.
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.