Skip to content

Prompt Caching

Every message you send an LLM carries the whole conversation with it — the API has no memory between requests, so the system prompt, the tool definitions, and every prior turn get re-read and re-priced on every single call. For a short chat that's fine. For an agent with a large system prompt, a dozen tool schemas, and a transcript that grows every turn, it means paying full price to re-process the same few thousand tokens hundreds of times in a row. Prompt caching is the fix: mark a point in the prompt, and the API reuses what it already processed up to that point instead of redoing the work.

The invariant: it's a prefix match

A cache entry is keyed on the exact bytes of the prompt up to a marked point. Change one byte anywhere before that point — a timestamp, a reordered JSON key, a different tool in the list — and the entry no longer matches. Everything from that byte onward reprocesses at full price, even if the rest of the prompt is identical to a hundred prior requests.

That single rule explains most of what follows. The API builds a request in a fixed order — tools, then system prompt, then messages — so a cache marker placed on the last system block covers tools and system together; one placed on the last message in a conversation covers everything before it. Whatever comes after the last marker is never cached, by design — that's where the part of the prompt that actually changes every turn belongs.

Placing a breakpoint

The marker is a cache_control block attached to a content block:

response = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    system=[{
        "type": "text",
        "text": LONG_SYSTEM_PROMPT,
        "cache_control": {"type": "ephemeral"},
    }],
    messages=[{"role": "user", "content": "Summarize the key points"}],
)

For a growing multi-turn conversation, the marker moves to the last block of the most recently appended turn each time — the API walks backward looking for the nearest prior cache entry, so earlier breakpoints stay valid and hits accumulate as the conversation gets longer. There's also a top-level cache_control on the request that auto-places the marker on the last cacheable block, which is the simplest option when fine-grained placement isn't worth the code.

A request allows up to four separate breakpoints — useful when a prompt has several stability tiers (a frozen system prompt, a per-session block of retrieved context, then the live conversation).

Reads are cheap, writes aren't free

A cache entry isn't free to create. Writing one costs a premium over a normal input token — more for a longer-lived entry — while reading a hit back costs a small fraction of the normal rate. That shapes when caching is worth it: a single request that's never repeated pays the write premium for nothing, since there's no second read to recoup it. It only pays off across multiple requests that share the same prefix, and a longer-lived cache needs more reuse to break even than a short-lived one, because the write costs more up front.

Two lifetimes are available: a default that lasts a few minutes, and a longer one for traffic with bigger gaps between requests. Pick the short one for anything conversational, where the next turn is seconds away, and the long one for bursty patterns — a batch job that fires every so often against the same shared context, say — where the requests are real but spaced out enough that the short lifetime would keep expiring between them.

There's a minimum size

A cache_control marker on a short prompt doesn't error — it just silently doesn't cache. Each model has its own minimum token count below which a marked prefix is too small to bother writing. It's easy to miss: the request succeeds, the response looks normal, and the only symptom is that the usage numbers never show a cache hit. Which is the actual way to tell whether caching is working at all — not by reading the code, but by reading the response.

Checking that it's actually happening

The response carries three numbers worth watching: tokens written fresh to cache, tokens served from a prior cache entry, and tokens that weren't cached at all. If the middle number stays at zero across a run of requests that should share a prefix, something upstream is invalidating the cache on every call, and the fix is to find that thing rather than to re-check the cache_control placement, which is probably fine.

What breaks it

The prefix-match rule means anything that changes the rendered bytes before a marker breaks the cache at and after that point. In practice, the usual culprits are the same everywhere:

  • A timestamp or request ID baked into the system prompt. "Current time: 14:32:07" at the top of the prompt means no two requests ever share a prefix.
  • Non-deterministic serialization. Dumping a dict without sorting keys, or iterating a set, can silently reorder the same logical content into different bytes from one call to the next.
  • A tool set that varies per request or per user. Tools render first, before the system prompt — adding, removing, or reordering even one tool invalidates everything downstream of it.
  • Switching models mid-conversation. Caches are scoped per model; a different model ID is a different cache, full stop.

Some of these invalidate more than others. The API actually has separate cache tiers for tools, system, and messages — a change to tool_choice or to whether images are present invalidates the system and message tiers but leaves the tools tier alone; a change to the tool list itself invalidates all three. So not every request-shape change is equally expensive, but the one thing that's always expensive is touching content that sits early in the render order.

Extended thinking and tool-heavy loops

Two things worth knowing if the agent uses thinking or runs long tool-use loops:

Thinking blocks participate in the same prefix rule — turning thinking on or off mid-conversation invalidates system and message caching the same way a tool_choice change does, so it's not something to toggle turn to turn if caching matters.

Long tool-use turns have a lookback limit. A breakpoint only looks back twenty content blocks to find its prior entry. An agentic loop that racks up more than twenty tool_use/tool_result pairs in a single turn can walk right past that window, and the next request's marker silently misses a cache entry that's still technically there — just further back than the API will look. The fix is the same one used for long system prompts: place an intermediate breakpoint every so often within the turn instead of only at the end.

The mid-conversation problem

The prefix rule creates one recurring conflict: agents often need to inject something dynamic — today's date, a mode switch, a fact learned mid-run — and the obvious place to put it is the system prompt, which is exactly the part of the request that sits earliest and is most expensive to invalidate. The usual fix is to stop treating the system prompt as mutable at all: keep it frozen, and put anything that changes into the message list instead, appended after the last cache marker rather than edited into the prefix. A one-line fact added at turn twelve costs nothing upstream of it; the same fact edited into the system prompt invalidates every turn that came before.