Prompt Caching with the Claude API: Breakpoints, TTL Economics, and Why It Silently Stops Working
Prompt caching lets you pay roughly a tenth of the normal input price for the parts of a prompt you send over and over — a long system prompt, a set of tool definitions, a document you keep asking questions about. The mechanism is simple, but it fails silently: when caching stops working, nothing errors, requests still succeed, and the only symptom is a larger bill. This guide covers how it actually works, where it breaks, and how to verify it is on.
The one rule everything follows from
Caching is a prefix match. The cache key comes from the exact bytes of the rendered prompt up to each breakpoint. Change one byte at position N, and every cache entry at or after position N is invalidated.
The render order is fixed: tools, then system, then messages. That ordering is the whole design constraint. Anything stable must physically come before anything volatile, because volatility poisons everything downstream of it.
This single rule explains most caching failures. A timestamp interpolated into the system prompt header does not just fail to cache itself — it makes the entire rest of the prompt uncacheable, no matter how many markers you place afterward.
Marking a breakpoint
You mark a cache breakpoint by attaching cache_control to a content block:
The value {"type": "ephemeral"} gives the default 5-minute TTL. Adding "ttl": "1h" gives a one-hour TTL. The marker can go on system text blocks, tool definitions, or message content blocks — text, image, tool_use, tool_result, and document blocks are all eligible.
Two constraints worth memorizing:
- Maximum 4 breakpoints per request. Budget them.
- A breakpoint on the last system block caches tools and system together, because tools render first. You do not need a separate marker on the tools.
There is also a top-level cache_control field on the request itself, which auto-places a breakpoint on the last cacheable block and moves it forward as the conversation grows. That is the right default for straightforward multi-turn chat. It is the wrong tool when your prompt ends in unique per-request content — the automatic breakpoint lands after the unique tail, so every request pays the write premium on bytes nobody ever reads back. In that case place an explicit marker at the end of the shared portion instead.
The minimum that silently swallows short prompts
A prompt shorter than the model's minimum cacheable prefix will not cache, and you get no error — just a cache-creation count of zero.
The minimum is model-dependent, and it is not monotonic across generations:
|
Model |
Minimum cacheable prefix |
|---|---|
|
Claude Opus 5, Fable 5 |
512 tokens |
|
Opus 4.8, Sonnet 5, Sonnet 4.6, Sonnet 4.5 |
1024 tokens |
|
Opus 4.7, Haiku 3.5 |
2048 tokens |
|
Opus 4.6, Opus 4.5, Haiku 4.5 |
4096 tokens |
A 3,000-token prompt caches on Claude Opus 5 and Sonnet 5, and silently does not on Opus 4.6 or Haiku 4.5. If you switch models and your cache hits vanish, check this table before auditing your code — the identical prompt can be cacheable on one model and invisible to the cache on another.
TTL economics: when the longer cache is worse
Cache reads cost about 0.1× the base input price. Writes cost 1.25× for the 5-minute TTL and 2× for the one-hour TTL. The write premium is why the longer TTL is not simply better.
Break-even follows directly:
- 5-minute TTL: two requests. One write plus one read is 1.35× versus 2× uncached.
- 1-hour TTL: three requests. 2× plus two reads is 2.2× versus 3× uncached.
The decision rule is the start-to-start gap between requests that share the prefix — not how long the conversation lasts:
- Under 5 minutes between request starts: use the 5-minute TTL. Every read refreshes the timer at no cost, so continuous traffic keeps a 5-minute entry alive indefinitely. The one-hour TTL buys nothing here except the doubled write price.
- 5 to 60 minutes: the one-hour TTL. This is the only window where the 2× write pays for itself.
- Over an hour: neither helps on its own. Re-warm on a schedule or accept the cold miss.
One detail that catches agent loops: the lifetime is measured from the start of the request that writes or reads the entry, and generation time counts against it. A turn that takes four minutes to generate leaves about one minute for the next request to start before a 5-minute entry expires.
The silent invalidators
These are the patterns to grep for in anything that builds the prompt prefix:
|
Pattern |
Why it breaks caching |
|---|---|
|
|
The prefix differs on every request |
|
Serializing a dict without sorting keys, or iterating a set |
Nondeterministic byte order |
|
A user or session ID interpolated into the system prompt |
Per-user prefix, no sharing across users |
|
Conditional system sections built with if-branches |
Every flag combination becomes a distinct prefix |
|
A tool list that varies per user or per mode |
Tools render at position 0 — nothing caches at all |
Two architectural rules prevent most of these:
Keep the system prompt frozen. Current date, mode, user name — none of it belongs in the system prompt. Inject dynamic context later in the message list, where it invalidates only what comes after it.
Do not change tools or model mid-conversation. Tools render first, so adding, removing, or reordering one invalidates everything. Caches are also model-scoped, so switching models starts from zero. If you need modes, pass the mode as message content rather than swapping the tool set.
There is a related trap in forked calls. Side computations — summarization, a sub-agent, a compaction pass — often build their own request. If the fork rebuilds system, tools, or model with any difference at all, it misses the parent's cache entirely. Copy the parent's values verbatim and append the fork-specific content at the end.
Verifying it actually works
The response usage object is the only ground truth:
cache_creation_input_tokens— tokens written this request, billed at the write premiumcache_read_input_tokens— tokens served from cache, billed at about 0.1×input_tokens— the uncached remainder only
That last one is the field people misread. Total prompt size is the sum of all three. If your agent ran for an hour and input_tokens reads 4K, the rest came from cache — check the sum, not the single field.
In a healthy multi-turn loop, the pattern is: reads grow turn over turn and cover the whole prior prefix; writes stay small, roughly the last assistant output plus the newly appended input; and uncached input is just the tail after the last breakpoint. If writes are instead near the full conversation size on every request, something upstream is rewriting the prefix.
Verify after every change, not just at setup. The expensive failure mode is a regression: caching works when you write it, then months later a new dynamic field lands in the system prompt and every request misses. Nothing errors. The bill is just higher. An integration test asserting that a second identical request shows cache_read_input_tokens above zero costs almost nothing and catches the entire class.
If reads are zero across repeated identical-prefix requests, log several consecutive request bodies and diff adjacent pairs. Strip the cache_control markers before diffing — the moving marker legitimately differs between requests and is not the culprit. The first real divergence inside the overlapping region is your invalidation point.
One thing to check when you route through a relay
Caches are isolated per workspace and never shared across organizations. Traffic for the same prompt split across two workspaces writes and reads separate entries — worth ruling out before you go hunting for a nondeterministic serializer.
If your requests pass through an OpenAI-compatible relay or gateway rather than going direct, the question of whether cache fields survive the round trip is empirical, not architectural: send the same prefix twice and read the usage fields off the second response. If cache_read_input_tokens comes back above zero, caching is working end to end. If the field is absent or always zero while a direct call to the same model shows reads, the caching semantics are not passing through your path, and that is worth knowing before you design a prompt layout around them.
ROIBest AI is an OpenAI-compatible API gateway for Claude and other models. Whatever path you use, the verification step above is the same — the usage fields on a second identical request are the only evidence that a cache is real.