Skip to content

Claude Prompt Caching in 2026: The Setup That Actually Cut My API Bill

Claude Prompt Caching in 2026: The Setup That Actually Cut My API Bill

Confession: I stared at last month’s Anthropic bill, made a small sad noise into my coffee, and then spent the rest of the morning re-reading the prompt caching docs like they owed me money. Turns out, they kind of did.

I’d been running a support-triage app on top of Claude for a while. Same 8k-token system prompt, same tool definitions, same handful of policy docs on every single request. I knew about cache_control. I’d even added it once, six months ago, and then never checked whether it was actually doing anything. Spoiler: it wasn’t, because I’d put the breakpoint in the wrong spot and Anthropic had quietly changed the default TTL under me.

This is the writeup I wish I’d read six months ago. What Claude prompt caching actually does, where I put cache_control in real requests, the 5-minute vs 1-hour TTL choice, what broke when I got greedy, and the actual line-item difference on my bill.

What prompt caching actually does (in one paragraph)

You mark a chunk of your prompt with cache_control. If the same prefix shows up again within the TTL, Claude serves that portion from a warm cache instead of re-processing it. Cache reads are cheap: about 10% of the input token price. Cache writes cost more than a normal input token, though: roughly 1.25× for the 5-minute TTL and 2× for the 1-hour TTL. So caching only wins when you’ll read the cached block enough times to amortize the write.

The rule of thumb I use, borrowed from Anthropic’s own guidance and confirmed by this dev.to writeup on the 2026 TTL shift: three reads to break even on 5-minute, five reads to break even on 1-hour. Below that, don’t bother.

Here’s a Python example using the SDK. Before:

from anthropic import Anthropic

client = Anthropic()

response = client.messages.create(
    model="claude-sonnet-5",
    max_tokens=1024,
    system=BIG_SYSTEM_PROMPT,  # 6k tokens, same every call
    messages=[{"role": "user", "content": user_msg}],
)

And after, with a single cache breakpoint:

response = client.messages.create(
    model="claude-sonnet-5",
    max_tokens=1024,
    system=[
        {
            "type": "text",
            "text": BIG_SYSTEM_PROMPT,
            "cache_control": {"type": "ephemeral"},
        }
    ],
    messages=[{"role": "user", "content": user_msg}],
)

That’s the whole change. On the first request you pay a small write premium. On every subsequent request inside the TTL window, the system prompt is basically free.

Where I actually put cache_control in real requests

The docs let you place up to four breakpoints in a single request. That flexibility is a trap if you don’t have a plan. Here’s the ordering I settled on, from most stable to least stable. Cache the stable stuff first, because a cache hit only counts if everything before the breakpoint also matches.

  1. Tool definitions. Change once a week at most. Always cache.
  2. System prompt. Change on deploys. Always cache.
  3. Long context documents (policy docs, code snippets, retrieved chunks). Cache if the same doc is going to show up in the next few requests.
  4. Multi-turn conversation history. Cache the last-but-one assistant turn if you’re doing multi-turn.

For a typical support-triage request, my breakpoints look like this:

response = client.messages.create(
    model="claude-sonnet-5",
    max_tokens=1024,
    tools=[
        # ... tool defs ...
        {"cache_control": {"type": "ephemeral"}}
    ],
    system=[
        {"type": "text", "text": SYSTEM_PROMPT,
         "cache_control": {"type": "ephemeral"}},
    ],
    messages=[
        {"role": "user", "content": [
            {"type": "text", "text": POLICY_DOC,
             "cache_control": {"type": "ephemeral"}},
            {"type": "text", "text": user_msg},
        ]},
    ],
)

Three breakpoints, one slot in reserve. Anthropic’s tool-use-with-prompt-caching guide has the pattern for cascading breakpoints across multi-turn tool loops if you need more sophistication than this.

TypeScript is almost identical, with the same shape:

const response = await client.messages.create({
  model: "claude-sonnet-5",
  max_tokens: 1024,
  system: [
    {
      type: "text",
      text: SYSTEM_PROMPT,
      cache_control: { type: "ephemeral" },
    },
  ],
  messages: [{ role: "user", content: userMsg }],
});

If you’re on the Vercel AI SDK, the providerOptions.anthropic.cacheControl field maps to the same thing. I wrote up how I use that stack in my post on the Vercel AI SDK v5.

The 5-minute vs 1-hour TTL choice

The default TTL is 5 minutes. If your traffic is bursty, like a customer opening a chat and sending 10 messages in a minute, 5 minutes is fine. Cheap writes, hits come easy.

The 1-hour TTL costs about 2× on writes but keeps the cache warm through longer gaps. You opt into it per breakpoint:

"cache_control": {"type": "ephemeral", "ttl": "1h"}

I use 1-hour on:
– Batch pipelines where I know I’ll hit the same system prompt every few minutes for an hour.
– Long-running evals where each iteration takes 4-6 minutes and I don’t want a 5-minute window ticking down every time.
– The tool definitions in my agent loops, which get called dozens of times over the course of a session but with unpredictable gaps.

I stick with 5-minute on:
– Interactive chat where cache hits are basically guaranteed inside a session.
– Anything where the “outer” prompt (tools + system) is small enough that a re-write is cheaper than paying the 1-hour premium.

One thing to watch: Anthropic changed the default TTL from 60 minutes to 5 minutes for Claude Code in early 2026, and it caused a lot of quiet cost regressions. If you have ENABLE_PROMPT_CACHING_1H=1 set anywhere in an old shell profile, know what it’s doing.

What broke when I got greedy

Three anti-patterns I ran into, in escalating order of expensive:

Cache-busting user data in the wrong slot. I once concatenated the user’s timestamp into the system prompt “for context”. Every request had a unique system prompt, so every request was a cache miss with a write premium. My bill went up. The fix is obvious in retrospect: keep dynamic data in the user message, not the system prompt.

Breakpoints on tiny blocks. I tried caching a 200-token instruction block and paid the write premium on every call. Anthropic requires a minimum block size (1024 tokens for Sonnet, 2048 for Opus) for caching to activate at all. Smaller blocks are silently ignored. If you’re not sure, log the cache_creation_input_tokens and cache_read_input_tokens fields on the response and check that they’re both non-zero over a window of requests.

Assuming workspace isolation matched my mental model. As of February 2026, caches are isolated per Anthropic workspace on the direct API, not per organization. If you have staging and production in the same org but different workspaces, they don’t share cache. That’s usually what you want, but I had a batch job that assumed the opposite and paid twice.

The actual line-item difference on my bill

Numbers from one of my triage services, comparing April (no caching) to May (with caching landed mid-April) with roughly matched request volumes:

  • Input tokens (uncached): dropped from 41M to 6M.
  • Cache read tokens: 0 to 34M.
  • Cache write tokens: 0 to 1.2M.
  • Total input spend: dropped about 68%.

Cache reads are billed at 10% of the input rate, so those 34M read tokens cost about what 3.4M normal input tokens would. Writes cost more per token but only fire once per new cache prefix. On this workload, my break-even calculation says caching pays for itself within four requests per prefix. I’m doing hundreds of requests per prefix per hour.

The Mager blog on how caching actually works has a similar breakdown with different numbers if you want to sanity-check the math against a second workload.

What I’d do this week if I were you

Open your Anthropic usage dashboard. If your app sends more than a handful of requests per session and you don’t see any cache_read_input_tokens in your response objects, you have easy money on the table. Try this:

  1. Identify the largest static chunk in your requests. Usually it’s the system prompt or a set of tool definitions.
  2. Add a single cache_control: {"type": "ephemeral"} block at the end of it.
  3. Log the usage object from the response for a day and check the ratio of cache reads to writes. You want reads at least 3× writes.
  4. Only then reach for more breakpoints or the 1-hour TTL.

Do the smallest version first. Prompt caching is one of those features where the naive implementation captures 80% of the savings, and every additional bit of cleverness has a smaller payoff and a larger blast radius.

If you’re doing this alongside a larger LLM-app rebuild, I keep notes on the whole stack (evals, guardrails, cost tracking) over on my work page. Ping me if you find a break-even calculation I got wrong.