Cut Your Claude Agent's Token Bill with Prompt Caching: A Practical Tutorial (2026) — Cesar Ayala
← All posts

Cut Your Claude Agent's Token Bill with Prompt Caching: A Practical Tutorial (2026)

Prompt caching reuses an already-processed prompt prefix, so you pay ~0.1x input price for the stable part (system, tools, context) instead of full price every turn. Add cache_control {"type":"ephemeral"} at the end of that prefix, keep the volatile question after it, run twice, and confirm usage.cache_read_input_tokens is non-zero.

The problem: your agent re-pays for the same prompt every turn

Here is the thing nobody tells you when you ship your first Claude agent: the Messages API is stateless. Every single turn, you re-send the whole system prompt, every tool definition, and the entire accumulated context as fresh input — and you pay full input price for all of it, every time, even though none of it changed.

Let me put a number on it so the bleeding is visible. Say your agent carries a 50,000-token stable prefix: a big system prompt, a fat set of tool definitions, maybe a chunk of shared context or a knowledge dump. On claude-opus-4-8 at $5 per million input tokens, that prefix costs about $0.25 of input every single turn. Ten turns in a conversation? You just paid ~$2.50 to re-process bytes that were byte-for-byte identical each time. Multiply by every user, every session, all month. That is real money leaking for nothing.

Prompt caching fixes exactly this. It reuses the already-processed prompt PREFIX, so you pay roughly 0.1x for the stable part instead of full price. That ~$0.25 read drops to about $0.025. Same output, lower bill, faster prefill.

Let me be honest up front, because I have seen people bolt caching onto the wrong prompt and wonder why nothing improved: caching only helps when there is a stable prefix to reuse. It does nothing for a prompt that changes from the first token. If your whole prompt is different every call, there is no prefix to match — skip it.

In this tutorial you will identify your stable prefix, add one cache_control marker in the right spot, verify the hit by reading usage, and dodge the gotchas that silently break it. This is the same cost discipline I apply when I put any LLM in production — it fits right into the broader cost of adding AI to your app.

How prompt caching actually works (prefix match, render order, breakpoints)

There is exactly one invariant, and everything else follows from it: caching is a prefix match. Claude hashes the prompt prefix up to your breakpoint. Any byte change anywhere in that prefix invalidates everything after it. One character moves, the whole cache after that point is gone.

To place the breakpoint correctly you have to know the render order. The API assembles your prompt as tools → system → messages, always in that order. So a breakpoint on your last system block caches your tools AND your system prompt together, because tools render before system. That is usually exactly what you want.

The rule of thumb: put stable content FIRST — the frozen system prompt, the deterministic tool list — and put volatile content (the user’s actual question, per-turn timestamps, per-turn state) AFTER the last cache_control breakpoint, with no marker on it.

Mechanically, cache_control: {"type": "ephemeral"} goes on a content block: a system text block, a tool, or a message block, sitting at the end of the stable prefix. If you do not need fine-grained control, you can pass a top-level cache_control on messages.create() and the SDK auto-caches the last cacheable block.

A few hard limits to keep in your head:

  • Max 4 cache breakpoints per request.
  • TTL is 5 minutes by default, or {"type": "ephemeral", "ttl": "1h"} for one hour.
  • Minimum cacheable prefix is per model: claude-opus-4-8 = 4096 tokens, claude-sonnet-4-6 = 2048, claude-haiku-4-5 = 4096. Below the minimum it silently will not cache — no error, cache_creation_input_tokens just stays 0. (These minimums move per model release — confirm yours in the docs.)
Where cache_control goes{"type":"ephemeral"} on the LAST block of the stable prefix
Render ordertools → system → messages (breakpoint on last system caches tools+system)
Max breakpoints4 per request
Read vs writeread ~0.1x input price · write 1.25x (5-min) / 2x (1-hour)
Min prefixopus-4-8 = 4096 · sonnet-4-6 = 2048 · haiku-4-5 = 4096 tok
Verify withusage.cache_read_input_tokens > 0
Prompt caching, the whole mental model on one card

The economics: read 0.1x vs write 1.25x, and when it pays off

Caching is not free on the first write — there is a small premium — so you want to know when it actually pays.

A cache READ costs about 0.1x the base input price. A cache WRITE costs 1.25x with the default 5-minute TTL, or 2x with the 1-hour TTL. So the first request that populates the cache pays a little extra; every subsequent request that reads it pays almost nothing.

Break-even with the 5-minute TTL lands at the second request: 1.25 (write) + 0.1 (read) = 1.35x for two requests, versus 2.0x if you had not cached at all. You are already ahead. With the 1-hour TTL the write costs double, so you need about three requests to break even: 2.0 + 0.2 = 2.2x versus 3.0x uncached.

Back to the worked example: that 50,000-token prefix on claude-opus-4-8 ($5/MTok input). Uncached, ~$0.25 of input per turn. The write turn costs ~$0.31 once (1.25 x $0.25); after that, every subsequent reuse reads the prefix at ~0.1x — about $0.025, roughly one-tenth of a normal uncached turn. Pennies.

For reference, the input/output prices that drive this math (per MTok): claude-opus-4-8 $5 / $25, claude-sonnet-4-6 $3 / $15, claude-haiku-4-5 $1 / $5.

When should you NOT reach for the 1-hour TTL? Only use it when your traffic has gaps longer than 5 minutes. If you have continuous traffic, each request keeps the 5-minute cache warm on its own, so you get the cheaper 1.25x write and never need to pay the 2x premium.

Uncached (full input)1
Cache write (5-min TTL)1.25
Cache read0.1
2 requests cached (1.25+0.1)1.35
2 requests uncached2
Relative cost on a stable prefix (1.0x = full uncached input price)

Step by step: add caching to a Claude agent (claude-opus-4-8)

Now the hands-on part. Let me start with the uncached call so you see what we are improving.

from anthropic import Anthropic

client = Anthropic()

# UNCACHED: this re-pays full input price for system + tools every turn
resp = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    system=[
        {"type": "text", "text": BIG_FROZEN_SYSTEM_PROMPT},  # 50K tokens, never changes
    ],
    tools=MY_TOOLS,
    messages=[
        {"role": "user", "content": "What's the CLABE for invoice 4471?"},
    ],
)

Now the fix. Step 1 — identify the stable prefix: the frozen system prompt plus the tool definitions plus any large shared context that does NOT change between turns. Step 2 — put cache_control: {"type": "ephemeral"} on the LAST block of that prefix (here, the final system text block). Because tools render before system, this one marker caches tools + system together. Step 3 — keep the volatile content (the user’s actual question, per-turn state) AFTER the breakpoint, with no marker.

resp = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    system=[
        {
            "type": "text",
            "text": BIG_FROZEN_SYSTEM_PROMPT,   # stable
            "cache_control": {"type": "ephemeral"},  # <-- breakpoint at end of prefix
        },
    ],
    tools=MY_TOOLS,  # tools render first (position 0), cached together with system
    messages=[
        # volatile content lives here, AFTER the breakpoint, with NO cache_control
        {"role": "user", "content": "What's the CLABE for invoice 4471?"},
    ],
)

Step 4 — run the same request twice within the 5-minute window so the second call can read what the first wrote. Step 5 — read usage on both responses: the first request writes the cache (cache_creation_input_tokens > 0), the second reads it (cache_read_input_tokens > 0).

print(resp.usage.cache_creation_input_tokens)  # > 0 on the first (write) call
print(resp.usage.cache_read_input_tokens)       # > 0 on the second (read) call
print(resp.usage.input_tokens)                  # only the UNCACHED remainder

A note on shape: passing system as a list of text blocks is what lets you attach cache_control to a specific block. A plain string gives you nowhere to hang the marker. If you do not need that precision, the simplest option is a top-level cache_control on messages.create(), which auto-places the breakpoint on the last cacheable block. This is the same production discipline I cover in Claude structured outputs in production — small API details, big reliability payoff.

  1. 1. Identify the stable prefixFrozen system prompt + tool definitions + large shared context that never changes between turns.
  2. 2. Mark the end of itPut cache_control {"type":"ephemeral"} on the LAST block of the prefix (the final system text block).
  3. 3. Keep volatile content afterThe user's question and per-turn state go AFTER the breakpoint, with no marker.
  4. 4. Run twiceFire the same request twice inside the 5-minute window so the second can read what the first wrote.
  5. 5. Confirm the hitCheck usage.cache_read_input_tokens is non-zero on the second call.
The add-caching workflow, in order

Verify the cache hit (and read the usage fields right)

This step is not optional, because a silent miss looks identical to a hit until you check usage. The response comes back fine either way; only the billing fields tell the truth. Three fields, precisely:

  • usage.cache_read_input_tokens — tokens served from cache this request. You paid ~0.1x for these.
  • usage.cache_creation_input_tokens — tokens written to cache this request. You paid ~1.25x (5-min) or ~2x (1-hour) for these.
  • usage.input_tokens — the UNCACHED remainder, billed at full price. This is NOT the total prompt size, which trips people up constantly.

The total prompt is the sum: input_tokens + cache_creation_input_tokens + cache_read_input_tokens. So if your agent ran for hours but input_tokens reads 4K while cache_read_input_tokens reads 48K, that is caching doing its job — the rest came from cache.

The diagnostic: if cache_read_input_tokens is 0 across repeated identical-prefix requests, a silent invalidator is breaking the prefix. The move is to diff the rendered prompt bytes between two requests and find what changed. And if cache_creation_input_tokens stays 0 even on the first request, your prefix is below the model’s minimum (1024 tokens on claude-opus-4-8) — it is simply too short to cache.

The gotchas that silently break caching

This is the part the docs fragment and the part that actually costs people money. Every one of these produces a cache_read of 0 with no error.

Silent invalidators in the system prompt. A datetime.now() / Date.now(), a uuid4() / randomUUID(), or a per-request id interpolated into the prefix changes it every single call, so NOTHING caches. Here is the classic mistake and its fix:

# BROKEN: the timestamp lives in the cached prefix, so it changes every call
system = [{
    "type": "text",
    "text": f"You are a billing agent. Current time: {datetime.now()}",
    "cache_control": {"type": "ephemeral"},
}]

# FIXED: freeze the system prompt; push the timestamp AFTER the breakpoint
system = [{
    "type": "text",
    "text": "You are a billing agent.",
    "cache_control": {"type": "ephemeral"},  # frozen prefix
}]
messages = [{
    "role": "user",
    "content": f"Current time: {datetime.now()}. What's the CLABE for invoice 4471?",
}]  # volatile content, no marker, lives after the prefix

Non-deterministic JSON. json.dumps without sort_keys=True (or iterating a set) makes the prefix bytes differ run to run. Serialize deterministically — json.dumps(obj, sort_keys=True).

Changing tools or the model mid-conversation. Tools render first at position 0, so swapping the tool set rebuilds everything. A model switch is a different cache entirely. Both force a full rebuild — pick your tools and model and hold them steady for the conversation.

The 20-block lookback. A breakpoint walks back at most 20 content blocks to find a prior cache entry. In long agent loops with many tool_use / tool_result pairs, you can blow past that window. Add an intermediate breakpoint every ~15 blocks to keep the chain alive (you have 4 breakpoints — use them).

Concurrent fan-out pays full price. The cache is not readable until the first response begins streaming. If you fire 10 requests at once, all 10 pay the write price. Await the first token of one request, then fire the rest so they read the warm cache.

And the honest limit, again: caching cuts cost and latency on a STABLE prefix only. For a prompt that changes from the first token there is no reusable prefix — do not add cache_control there, or you will only pay the write premium with zero reads. Always confirm current pricing and minimums in the Anthropic prompt caching docs. This applies to any AI agent whose bill you are trying to cut.

Caches (do this)

  • Frozen system prompt — no timestamps, no UUIDs
  • Deterministic tool list, held steady all conversation
  • JSON serialized with sort_keys=True
  • Volatile question kept after the last breakpoint

Silently breaks it (avoid)

  • datetime.now() or uuid4() inside the prefix
  • A per-request tool set that varies each call
  • A model switch mid-conversation
  • Concurrent fan-out before the first response streams
What caches vs what silently breaks it

FAQ

Why is cache_read_input_tokens always 0? Either a silent invalidator (a timestamp, a UUID, or unsorted JSON in the prefix) is changing the bytes every call, or your prefix is below the model minimum (1024 tokens on claude-opus-4-8). Diff the rendered prompt between two requests to find the culprit.

Does caching change the model’s output? No. It only changes how the stable prefix is billed and how fast prefill runs. The response is identical.

How many breakpoints can I use? Max 4 per request. For most agents, one on the last system block is plenty; reach for more only in long loops to beat the 20-block lookback.

5-minute or 1-hour TTL? Default 5-minute for continuous traffic. Use 1-hour only for bursty traffic with gaps longer than 5 minutes — but remember the write costs 2x instead of 1.25x.

Is it worth caching a prompt that changes every turn? No. With no reusable prefix you only pay the write premium with zero reads. Leave cache_control off.

Which models, what minimums? As of the current docs, claude-opus-4-8 = 4096 tokens, claude-sonnet-4-6 = 2048, claude-haiku-4-5 = 4096. Below the minimum it silently will not cache. These move per model release — confirm current values in the docs.

Wrapping up

The whole play fits in one line: freeze the prefix, put cache_control at its end, keep the question after it, run twice, confirm cache_read_input_tokens > 0. Do that and your stable part drops from full input price to ~0.1x, with prefill latency falling alongside it.

Two reminders before you ship. First, these numbers move — always confirm current pricing and per-model minimums against the primary sources: the Anthropic prompt caching docs and the Anthropic pricing page. Second, caching is one lever; if you want more agent guides in the same practical vein, I keep them over at AI agents.

That is the cheapest win in your whole Claude stack. Go freeze that prefix.