I moved a scoring job from Sonnet 4.5 to Haiku 4.5 to cut its cost by two thirds. A week later the bill had dropped, but nowhere near as much as my spreadsheet promised. The cause was sitting in every response I'd logged: cache_read_input_tokens: 0. All 1,240 calls.
No error. No warning. The cache_control marker was still on the system prompt. I had hit the Claude prompt caching minimum: Haiku 4.5 won't cache a prefix shorter than 4,096 tokens, and mine was 3,180.
TL;DR
- Claude prompt caching has a minimum cacheable prefix length that depends on the model. Below it,
cache_controlis silently ignored: no error, justcache_creation_input_tokens: 0andcache_read_input_tokens: 0. - The minimum does not shrink with model size: 1,024 tokens on Sonnet 4.5, 4,096 on Haiku 4.5. Moving to the "smaller" model quadrupled the threshold.
- Silent misses made my Haiku calls cost 82% more than forecast ($0.0063 vs $0.0035 per call).
- The fix: measure the prefix with
count_tokensagainst the target model at startup, and alert when both cache counters read zero. - Crossing the line with useful content (two calibration examples, 4,410 tokens) made each call 38% cheaper than the shorter, uncached prompt.
What was I trying to cache?
The job is the answer scorer inside Preterview, a voice interview practice app I run (full disclosure: I built it). After each spoken answer, the scorer reads the transcript and returns a structured evaluation; at the end of a session it writes the report. A typical session makes about 9 scoring calls, so a stable, cacheable prefix is exactly the kind of thing that should make this cheap.
Each scoring call looked like this:
| Part | Tokens | Changes per call? |
|---|---|---|
| Tool definition (report JSON schema) + system prompt (rubric, 3 interviewer styles, output rules) | 3,180 | No |
| Question + answer transcript | ~1,400 | Yes |
| Output | ~350 | Yes |
On Sonnet 4.5 the 3,180-token prefix cached fine. I never thought about it again, which turned out to be the problem.
What is the Claude prompt caching minimum for each model?
The Claude prompt caching minimum is the shortest prefix (everything up to your cache_control breakpoint, in render order: tools, then system, then messages) that the API will write to cache. Shorter prefixes are processed normally and billed at the full input price.
| Model | Minimum cacheable prefix |
|---|---|
| Sonnet 4.5, Sonnet 4.6, Opus 4, Opus 4.1, Opus 4.8 | 1,024 tokens |
| Opus 4.7, Haiku 3.5 | 2,048 tokens |
| Haiku 4.5, Opus 4.5, Opus 4.6 | 4,096 tokens |
| Current generation (Opus 5.5, Sonnet 5.5, Haiku 5.5, Fable 5.x) | 512 tokens (verify against the docs for your exact model) |
Read that table twice. It isn't sorted by model size or by release date. Haiku 4.5 needs four times the prefix Sonnet 4.5 does. Opus 4.7 needs half of what Opus 4.6 needed. A prompt that caches on one model can silently stop caching on its neighbor.
Two details that bit me while reading the fine print:
- Tools count. My JSON schema tool definition sits before the system prompt in the rendered request, so it's part of the prefix. Without it I'd have been even shorter.
-
The marker isn't validated. Putting
cache_controlon a 3,180-token block for a 4,096-token model is a valid request. The API doesn't reject it or warn you.
How much did the silent cache miss cost?
The dollars were small at my volume. The ratio is what scales. Here's the per-call math, using list prices (Sonnet 4.5 at $3/$15 per million tokens, Haiku 4.5 at $1/$5, cache reads at 0.1x the input price):
| Scenario | Per call | 1,240 calls |
|---|---|---|
| Sonnet 4.5, prefix cached (before) | $0.0104 | $12.90 |
| Haiku 4.5, prefix cached (forecast) | $0.0035 | $4.30 |
| Haiku 4.5, nothing cached (actual) | $0.0063 | $7.85 |
The forecast assumed the 3,180 prefix tokens would cost like 318 tokens. Instead every call paid for all 3,180 at full price. Actual spend came in 82% above forecast.
Why didn't I notice for a week?
Because nothing failed, and the number I watched still went down.
My dashboard tracked total spend per day. Compared to Sonnet, spend dropped 39%. That looks like a win if the forecast isn't sitting next to it. I was expecting roughly 67%, and I'd have caught the gap sooner if I'd charted forecast against actual instead of this week against last week.
I'd also never checked the prefix length against the new model. On Sonnet the threshold was 1,024 and my prompt was three times that. In my head, "long enough to cache" was a property of the prompt. It's a property of the prompt and the model.
How do you check whether a prompt is long enough to cache?
Count it against the model you'll actually call, and fail loudly at startup. messages.count_tokens takes the same system and tools you send, so the number includes the tool schema:
import anthropic
client = anthropic.Anthropic()
# Copy the values for your models from the prompt caching docs.
MIN_CACHEABLE = {
"claude-sonnet-4-5": 1024,
"claude-haiku-4-5": 4096,
}
def assert_cacheable(model, system, tools):
n = client.messages.count_tokens(
model=model,
system=system,
tools=tools,
messages=[{"role": "user", "content": "."}],
).input_tokens
floor = MIN_CACHEABLE[model]
if n < floor:
raise RuntimeError(
f"{model}: cacheable prefix is ~{n} tokens, minimum is {floor}. "
"cache_control will be silently ignored."
)
return n
Don't estimate with characters divided by four, and don't use another vendor's tokenizer. Use the API's own count for the model you deploy.
Then watch it at runtime, because prefixes drift as people edit prompts:
u = resp.usage
if u.cache_creation_input_tokens == 0 and u.cache_read_input_tokens == 0:
silent_misses += 1
if silent_misses >= 20:
alert(f"No cache write or read in 20 calls on {resp.model}")
else:
silent_misses = 0
A miss with a cache write is normal: that's a cold start. A miss with zero write and zero read on a request that has a cache_control marker means the cache isn't working at all: below the minimum, or a breakpoint in the wrong place.
Should you pad a prompt to reach the caching minimum?
If the padding is useful content, the math says yes. A cached read costs 0.1x, so 4,410 cached tokens bill like 441 uncached ones. Making the prompt longer made it cheaper.
I didn't add filler. I added something I'd been putting off: two fully scored calibration answers, one strong and one weak, so the scorer sees what each rubric level looks like. That took the prefix from 3,180 to 4,410 tokens, comfortably over 4,096 with room for edits.
Over the next 1,310 calls:
- 94% read the prefix from cache. The misses were cold starts after quiet stretches longer than the 5-minute TTL, mostly overnight.
- Per-call cost, including the 1.25x cache writes on those misses: about $0.0039, against $0.0063 for the shorter uncached prompt. That's 38% cheaper.
Did the calibration examples also make scores more consistent? It seemed that way, but I didn't run a proper eval on it, so I'm not claiming a number.
One caveat: caches are scoped to the model. Every model switch starts cold, and every model switch means rechecking the minimum. If you move to the current generation, the 512-token floor makes this problem much rarer, but the startup check costs nothing and catches the next surprise.
So why did 1,240 Haiku 4.5 calls get 0 cache hits?
Because the Claude prompt caching minimum on Haiku 4.5 is 4,096 tokens and my cacheable prefix (tool schema plus system prompt) was 3,180, so the API silently skipped caching on every request while still accepting the cache_control marker. The same prompt cached on Sonnet 4.5, whose minimum is 1,024. The fix is to measure the prefix with count_tokens against the exact model you deploy, alert when cache_creation_input_tokens and cache_read_input_tokens are both zero, and, if you're just under the line, add useful content until you cross it. A longer prompt that caches is cheaper than a shorter one that doesn't.
Written by the developer behind Preterview, an interview prep platform.

Top comments (1)
The marker still sitting on the system prompt is the part I recognise. I had the same shape in a benchmark: the config said a feature was on, and the run wasn't the run the config described. Since then a run only counts if its output shows the feature was actually on. Your two counters are that check for caching.
One mode I think the zero-zero alert would miss is a prefix that changes on every call. Someone adds today's date or a session ID to the system prompt, and from then on every request writes the cache and none reads it. The write counter is never zero, so the alert stays quiet, and each call pays 1.25 times the prefix instead of once, which is more than sending it uncached.
Your own numbers suggest a sharper rule. Your misses were cold starts after quiet stretches longer than the TTL. So a write after a quiet stretch is normal, and a write when the previous call finished a minute ago is the one worth an alert.