# Prompt cache floors: 512 on Opus 5, 4,096 on Haiku 4.5

> Anthropic's minimum cacheable prefix runs backwards: newer models have the lower floor, so one prefix caches on Opus 5 and silently never on Haiku 4.5.

URL: https://bartlomiejkrupa.dev/articles/prompt-cache-floors-by-model/
Published: 2026-07-29
Updated: 2026-10-07
Tags: prompt-caching, cost, tokens, claude, cheatsheet, cache-control
Summary: Anthropic's minimum cacheable prefix runs backwards: newer models have the lower floor, so one prefix caches on Opus 5 and silently never on Haiku 4.5.

Anthropic's minimum cacheable prefix is model-dependent, and the ordering is the opposite of the obvious guess: the newest and most expensive models have the **lowest** floor. A prefix below the floor is not cached, no error is raised, and `cache_creation_input_tokens` stays at 0.

| Minimum prefix | Models |
| --- | --- |
| **512 tokens** | Opus 5.5, Opus 5, Fable 5.1, Fable 5, Mythos 5.1, Mythos 5, Sonnet 5.5 |
| **1,024 tokens** | Opus 4.8, Sonnet 5, Sonnet 4.6, Sonnet 4.5, Opus 4.1, Opus 4, Sonnet 4 |
| **2,048 tokens** | Opus 4.7, Mythos Preview, Haiku 3.5 |
| **4,096 tokens** | Opus 4.6, Opus 4.5, Haiku 4.5 |

These floors apply on the Claude API, Claude Platform on AWS, Google Cloud, and Microsoft Foundry. On Amazon Bedrock, AWS documents its own per-model minimums - check the [Bedrock prompt caching guide](https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html) before you rely on these numbers there.

## The routing trap

The non-monotonic ordering has one practical consequence worth more than the table itself.

A 3,000-token policy prefix caches on Opus 5 and **silently never caches on Haiku 4.5**. So the moment you do the sensible thing and fan high-volume work out to the cheap tier, that tier starts paying full input price on every call. The cost model you validated on the expensive model does not transfer down.

Check the floor for the model you **route to**, not the one you developed against. If you are tiering work across models, this belongs in the same review as the price table in [Stop paying full price for repeat LLM calls](/articles/llm-cost-optimization).

Opus 5 moved the other way: it halved the Opus 4.8 floor from 1,024 to 512, so prompts previously too short to cache now create entries with no code change. Sonnet 5.5 did the same for the Sonnet line - 512 instead of Sonnet 5's 1,024 - so a 700-token prefix that never cached on Sonnet 5 caches after the model swap. Worth re-checking anything you wrote off as uncacheable.

## Verify, don't assume

Three fields in `usage` tell you what actually happened:

| Field | Meaning |
| --- | --- |
| `cache_creation_input_tokens` | Written to cache this request (paid the write premium) |
| `cache_read_input_tokens` | Served from cache this request (paid the read rate, 0.1x or less) |
| `input_tokens` | Processed at full price - the **uncached remainder only** |

Total prompt size is `input_tokens + cache_creation_input_tokens + cache_read_input_tokens`. That last point catches people out: if a long-running agent reports 4,000 `input_tokens`, the rest was served from cache. Check the sum, not the single field.

The one-line diagnosis: **if `cache_read_input_tokens` is 0 across repeated identical requests, either the prefix is changing or it is under the floor.** Both are silent. The [cache-read verifier](/notes/cache-read-verifier) turns that check into a two-call test.

```python
u = client.messages.create(**build_request()).usage
print(u.cache_creation_input_tokens, u.cache_read_input_tokens, u.input_tokens)
# call 1: write>0, read=0    -> healthy, cache written
# call 2: write=0, read>0    -> healthy, cache read
# call 2: write>0, read=0    -> prefix changing every call
# both 0 across calls        -> prefix is under the model's floor
```

To tell "changing" from "too short", build the request twice and diff it - the byte that moved is rarely the suspected one. Full procedure in [Don't break the prompt cache](/articles/dont-break-the-prompt-cache); the one-line rule is [cache-stable prefix](/notes/cache-stable-prefix).

## Breakpoints are a budget of four

Maximum **4** `cache_control` breakpoints per request, counted across `tools` + `system` + `messages` combined. Top-level automatic caching consumes one slot.

Exceeding the limit returns a 400, which is the good outcome. The bad outcome is an agent loop that appends a breakpoint per turn as history grows, exhausts the budget mid-session, and starts rewriting the prefix instead of reading it.

Render order is `tools` -> `system` -> `messages`, and caching matches everything up to and including a marked block. So spend the budget from the front: one breakpoint after tool schemas, one after the system prompt, and give a rolling message window whatever remains.

```python
system=[{
    "type": "text",
    "text": SYSTEM_PROMPT,
    "cache_control": {"type": "ephemeral"},           # 5-minute TTL (default)
    # "cache_control": {"type": "ephemeral", "ttl": "1h"},
}]
```

## What invalidates what

Not every parameter change costs you the whole cache. There are three tiers, and a change only invalidates its own tier and below.

| Change | Tools cache | System cache | Messages cache |
| --- | --- | --- | --- |
| Tool definitions added, removed, reordered | lost | lost | lost |
| Model switch | lost | lost | lost |
| System prompt content | kept | lost | lost |
| Web search, citations, or `speed` toggled | kept | lost | lost |
| `tool_choice`, images | kept | kept | lost |
| Thinking config or effort changed | model-specific | model-specific | lost |
| Message content | kept | kept | lost |

The useful read: you can flip `tool_choice` or swap images per request without losing the tools-and-system cache. Thinking and effort changes always cost the messages cache, and on some models the tools and system caches too - so set them once per session. Only tool-definition and model changes are guaranteed to force a full rebuild.

Two of those rows have an escape hatch, and they are gated separately:

- **System prompt content** - append a `{"role": "system", "content": "..."}` message to `messages` instead of editing top-level `system`. Available with no beta header on Fable 5.1, Mythos 5.1, Fable 5, Mythos 5, Opus 5.5, Opus 5, Opus 4.8, and Sonnet 5.5. **Not** on Sonnet 5 - use the top-level `system` field there.
- **Tool definitions** - send the new or changed definition in a `tool_addition` block inside a mid-conversation system message, behind the `inline-tools-2026-09-15` beta header, and leave `tools` exactly as you first sent it. One exception: if your `tools` array has no non-deferred tool, the first tool defined this way costs one full cache miss.

Model switching has no escape hatch: caches are model-scoped. Keep the main loop on one model and push cheaper sub-tasks to a [subagent](/notes/subagent-context-isolation).

## Two silent failures that are not the floor

**The 20-block lookback.** Each breakpoint walks back at most 20 content blocks looking for a prior cache entry. A single turn that appends more than 20 blocks - routine in an agent loop with many tool-use and tool-result pairs - pushes the previous entry out of range, and the next request silently misses. Fix: place an intermediate breakpoint roughly every 15 blocks in long turns.

**Concurrent requests all pay.** A cache entry becomes readable only once the first response **begins streaming**. Fire N identical-prefix requests in parallel and every one of them pays full price, because none can read what the others are still writing. For fan-out, send one request, wait for its first streamed token, then release the rest.

## Economics, and when caching is a loss

Cache reads cost 0.1x base input on most models, 0.05x on Opus 5.5 and 0.025x on Fable 5.1 - cheaper reads lower the break-even slightly, never raise it. Writes cost **1.25x for the 5-minute TTL and 2x for the 1-hour TTL**, which sets different break-even points:

| TTL | Write cost | Break-even |
| --- | --- | --- |
| 5 minutes | 1.25x | 2 requests (1.25 + 0.1 = 1.35x vs 2x uncached) |
| 1 hour | 2x | 3 requests (2 + 0.2 = 2.2x vs 3x uncached) |

So caching a prefix you call exactly once is a pure loss: you pay the write premium and never read it. The 1-hour TTL keeps entries alive across gaps in bursty traffic, but the doubled write means it needs more reads to pay for itself.

**Pre-warming.** To remove the cold-write latency from the first real request, send a `max_tokens: 0` request at startup. It runs prefill, writes the cache, returns immediately with empty `content`, and bills no output tokens. Put `cache_control` on the last block shared with the real request - the system prompt or tool schemas - never on the placeholder user message. It is rejected with `stream: true`, with thinking explicitly enabled, with `output_config.format`, with a forced `tool_choice`, and inside a Batches request.

Only pre-warm when first-request latency is user-visible and there is a quiet moment before traffic. If requests already arrive more often than the TTL, they keep the cache warm themselves and a separate warm call is pure extra write.

## Next move

Look up the floor for whatever model your highest-volume endpoint routes to, then log `cache_read_input_tokens` on it for a day. If it reads 0, you now have two candidate causes and a way to tell them apart. The placement discipline is in [Don't break the prompt cache](/articles/dont-break-the-prompt-cache), the output side of the bill is in [Trim output, not the cache](/articles/trim-output-not-the-cache), and the full lever map is on the [Optimization](/topics/optimization) hub.

Sources:
- Prompt caching: https://platform.claude.com/docs/en/build-with-claude/prompt-caching
- Mid-conversation system messages: https://platform.claude.com/docs/en/build-with-claude/mid-conversation-system-messages
- Pricing: https://platform.claude.com/docs/en/about-claude/pricing
- Prompt caching for Amazon Bedrock: https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html
