How Claude's prompt cache works, where to put cache breakpoints, and how to check that you are actually getting cache hits.
Most LLM requests in a real application repeat themselves. The system prompt is the same on every call, a RAG answer re-sends the same instructions, and a chat re-sends the entire conversation each turn. Prompt caching lets Claude reuse the work it already did on that repeated prefix: cached input is billed at a fraction of the normal price and the response starts sooner.
How the cache works
The cache is a prefix match. You mark a point in the prompt with cache_control, and Claude caches everything up to that point. A later request that begins with exactly the same bytes reads from the cache instead of processing them again.
The prompt is rendered in a fixed order: tools, then system, then messages. A breakpoint on the last system block therefore caches your tool definitions and system prompt together.
Because it is a byte-level prefix match, any change anywhere in the prefix invalidates everything after it. That rule drives every design decision below.
The simplest setup
For most apps, automatic caching is enough. Add cache_control at the top level of the request and Claude places the breakpoint on the last cacheable block, moving it forward as a conversation grows:
import Anthropic from "@anthropic-ai/sdk"
const client = new Anthropic()
const response = await client.messages.create({
model: "claude-sonnet-4-5",
max_tokens: 16000,
cache_control: { type: "ephemeral" },
system: LONG_SYSTEM_PROMPT,
messages,
})Explicit breakpoints for a shared prefix
Automatic caching puts the breakpoint at the end of the prompt. That is wrong when the end is unique to each request, for example a retrieved document plus a one-off question. Every request would write a new cache entry that is never read again.
Put an explicit marker at the end of the shared part instead:
const response = await client.messages.create({
model: "claude-sonnet-4-5",
max_tokens: 16000,
system: [
{
type: "text",
text: INSTRUCTIONS_AND_EXAMPLES, // identical on every request
cache_control: { type: "ephemeral" },
},
],
messages: [
// Varies per request, so it sits after the breakpoint.
{ role: "user", content: `${retrievedContext}\n\nQuestion: ${question}` },
],
})You can use up to four breakpoints per request, which is useful when sections change at different rates: tools never, a knowledge base daily, the conversation every turn.
Pricing and TTL
Caching has a write cost and a read discount:
| Cost relative to normal input | |
|---|---|
| Cache write, 5-minute TTL (default) | 1.25x |
| Cache write, 1-hour TTL | 2x |
| Cache read | about 0.1x |
With the default TTL, two requests sharing a prefix already come out ahead. Every cache read also resets the timer, so traffic that reuses a prefix at least every five minutes keeps it warm indefinitely.
Choose the TTL by the gap between requests that share the prefix:
- Under 5 minutes (busy endpoints, agent loops): use the default. It is strictly cheaper.
- 5 to 60 minutes (a user who replies after a coffee break): use
ttl: "1h", the only case where the doubled write pays off.
cache_control: { type: "ephemeral", ttl: "1h" }Minimum length
Very short prefixes are not cached at all, and there is no error when that happens. The minimum depends on the model: 1,024 tokens on Claude Sonnet 4.5, and up to 4,096 tokens on others such as Claude Haiku 4.5. If you are optimizing a short system prompt, check the minimum for that model first.
Verify that it works
The response usage object is the only ground truth:
const { usage } = response
console.log({
written: usage.cache_creation_input_tokens, // paid the write premium
read: usage.cache_read_input_tokens, // served from cache
uncached: usage.input_tokens, // full price
})On a second identical request you should see cache_read_input_tokens greater than zero. Note that input_tokens is only the uncached remainder; the full prompt size is the sum of all three fields.
Add a test that sends the same request twice and asserts
cache_read_input_tokens > 0. Caching failures are silent: requests keep
working and only the bill changes, often after an unrelated refactor.
Common cache killers
If reads stay at zero, something in the prefix changes between requests. The usual suspects:
- A timestamp or request ID in the system prompt. Move dynamic values after the last breakpoint, or into the user message.
- Non-deterministic serialization. Building JSON from an object with unstable key order produces different bytes for the same data.
- A tool list that varies. Sort tools and keep their definitions stable; tools render first, so a change there invalidates everything.
- Splitting traffic across workspaces. Caches are isolated per workspace, so the same prompt sent from two workspaces caches twice.
The fix is always the same: log two consecutive request bodies, diff them, and move whatever differs to after the breakpoint.