Command Palette

Search for a command to run...

0GitHub stars
Blog

Cutting Claude latency and cost with prompt caching

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 TTL2x
Cache readabout 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.

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.

Command Palette

Search for a command to run...