Prompt Caching (Part 4): AWS Documents 4,096 — I Measured 1,024
AWS gives Claude Sonnet 4.5 a 4,096-token minimum cacheable prefix. Bisecting on Bedrock put the real threshold at 1,024. Three control models matched their docs; only this row was off.
Verdict, before the walkthrough.
AWS documents the minimum cacheable prefix for Claude Sonnet 4.5 as 4,096 tokens. I bisected it on Bedrock: the real threshold is 1,024 — the same figure as Anthropic’s first-party API. That row is off by 4×.
This is not a general “documentation is unreliable” claim. I ran the same harness against four models and three agreed with their documented figure; only Sonnet 4.5 disagreed. So the useful conclusion is the narrower one: a threshold is a number you have to measure once yourself, and measuring it takes two requests.
The practical payoff: if you ever wrote a prompt off as too short to cache, it may have been cacheable all along. And caching is worth switching on — five turns against one 4,470-token prefix cost $0.067050 without it and $0.022237 with it, 66.8% less — but not unconditionally, because the first turn is 25% more expensive.
The whole experiment ran from an EC2 instance over the AWS CLI. The Sonnet 4.5 portion was 16 calls and cost $0.0887.
Tested on#
Test date 2026-08-17, us-east-1, US inference profile
Models Claude Sonnet 4.5, Sonnet 4.6, Sonnet 5, GPT-5.6 Terra
How AWS CLI v2.36.23 from an EC2 instance
Claude: bedrock-runtime converse (cachePoint)
GPT-5.6: bedrock-runtime invoke-model (prompt_cache_breakpoint)
Hit criterion cacheWriteInputTokens or cacheReadInputTokens non-zero in usage
What I paid $0.0887 for the 16-call Sonnet 4.5 bisect;
under $0.20 for everything in this article
Funding self-funded. Not sponsored, no vendor credits, no vendor review.
Prices used throughout are the Sonnet 4.5 multipliers in effect on the test date: normal input 1× at $3 per million tokens, cache write 1.25×, cache read 0.1×.
What is actually cached#
A claim that circulates widely: prompt caching saves you the cost of re-sending repeated tokens. That is not what happens. The prompt is transmitted in full every time, to the byte.
What the server does with it has two phases. First it processes the entire input in one parallel pass — prefill, which is compute-bound. Only then does it emit output one token at a time — decode, which is memory-bandwidth-bound. Prefill leaves behind the K/V projection of every token, the KV cache. That is the artefact prompt caching keeps.
So what you stop paying for is the server’s own recomputation, and it shows up on the bill as a lower per-token input rate. Nothing about network transfer changes.
The load-bearing word is identical prefix. Attention is causal: change one token and every KV after it is invalid, so the cache cannot be picked up downstream of the edit.
Which yields exactly one layout rule: stable content first, volatile content last. System rules, tool definitions and reference material in front; this turn’s question and anything carrying a clock behind. A timestamp injected at the top of the prompt zeroes the entire cache, silently, and your bill simply goes up.
If a gateway or proxy sits between your code and Bedrock, that is another place a byte-stable prefix quietly stops being byte-stable — re-serialising the JSON is enough to reorder it. I have debugged that exact class of corruption in a proxy before, and caching degrades the same way it did there: no error, just a worse outcome.
Switching it on#
In the Bedrock Converse API you insert a cachePoint after the content you want cached.
"content": [
{ "text": "<stable system rules and reference material>" },
{ "cachePoint": { "type": "default" } },
{ "text": "<this turn's question>" }
]
Everything before the breakpoint is cached; everything after it is free to change. Ordering matters too, and the AWS documentation is explicit about it: breakpoints are processed tools → system → messages, and the minimum token count is evaluated across all three combined, not per block.
I called it the least clever way possible, straight from the CLI:
aws bedrock-runtime converse \
--region us-east-1 \
--cli-input-json file://body.json
The step you cannot skip#
Verification is not optional here, for one reason: when caching does not engage, nothing fails.
The documentation says as much — if the prefix is under the minimum, inference proceeds normally and simply is not cached. You get a 200, a correct answer, and no indication whatsoever. You just pay full price on every call.
Three fields in the response usage tell you what happened:
inputTokens the part after the breakpoint, not counted as cache
cacheWriteInputTokens tokens written into the cache on this call
cacheReadInputTokens tokens served from the cache on this call
There is a trap in the first one. With caching on, inputTokens is no longer your total input. On a hit, my 4,470-token request reported inputTokens of 11. The total is the sum of all three:
total input = inputTokens + cacheReadInputTokens + cacheWriteInputTokens
Cost a request off inputTokens alone and you will be out by an order of magnitude.
My acceptance test is: send the same prefix twice unchanged. Call one should show a write, call two should show a read. Both zero means it never cached.
Then one more step, which is the one that actually made the result trustworthy: send every prefix again with no cachePoint at all, as a control. If the cache counters are non-zero without a breakpoint, whatever I am reading is not the cache. They were zero in every control run. Only then does the measurement stand up.
The documented threshold is wrong#
I hit the silent-failure case for real, and it is how I found the bad row.
An early attempt used a prefix just under a thousand tokens. All three cache fields came back zero. The request succeeded, the answer was fine, nothing looked wrong. The AWS table gives Claude Sonnet 4.5 a minimum cacheable prefix of 4,096 tokens, which seemed to explain it — until slightly longer prefixes, still far below 4,096, cached without complaint.
At that point there were two live possibilities: the documentation is wrong, or my method is. Distinguishing them needs points, not opinions. So I bisected.
totalInput cached?
566 no
779 no
944 no
999 no ← lower bound
1,054 yes ← upper bound, the crossover
1,211 yes
1,541 yes
2,421 yes the docs say this should not cache
3,741 yes the docs say this should not cache either
4,401 yes
The crossover is pinned between 999 and 1,054, which is 1,024. And 2,421 and 3,741 are direct counter-evidence on their own: both are below the documented 4,096, and both cached normally.
One caution on building these prefixes: tokenizers differ per model. The same 70 text segments came to 779 tokens on Sonnet 4.5 and 1,271 on Sonnet 5, so the repeat count has to be calibrated per model. Take the size from what the API reports, not from an estimator — or measure your real system prompt with the token counter first so you know which range is even worth probing.
Three controls, all matching their docs#
Testing only Sonnet 4.5 could not separate “this row is wrong” from “the documentation is unreliable”. So I ran three more models through the same harness.
| Model | Documented minimum | Highest not cached | Lowest cached | Verdict |
|---|---|---|---|---|
| Claude Sonnet 4.5 | 4,096 | 999 | 1,054 | Docs 4× too high |
| Claude Sonnet 4.6 | 1,024 | 1,000 | 1,055 | Matches |
| Claude Sonnet 5 | not listed | 983 | 1,055 | Measured 1,024 |
| GPT-5.6 Terra | 1,024 | 817 | 1,504 | Matches |
All four models really threshold at 1,024. The only figure in the documentation that disagrees with measurement is the 4,096 on the Sonnet 4.5 row.
That distribution is what makes the conclusion safe to state. It is not my method drifting, because three controls would have drifted with it. It is not Bedrock’s documentation being broadly unreliable, because three rows were right. It is one specific value being wrong. Whether it was mistyped originally or lowered after launch without the page being updated is not something I can see from outside, and I am not going to guess.
How quiet the failure really is#
This is the part worth internalising. Here is the complete usage from the 999-token call:
{
"inputTokens": 999,
"outputTokens": 8,
"totalTokens": 1007,
"cacheReadInputTokens": 0,
"cacheWriteInputTokens": 0
}
No error. No warning. No “your cachePoint was ignored because…”. The model answered normally and totalTokens looks entirely reasonable. Unless you go looking at those two cache fields, everything appears fine.
GPT-5.6 was equally quiet at 817 tokens, only with different field names — it reports under usage.prompt_tokens_details.{cached_tokens, cache_write_tokens}.
One platform difference worth knowing, because the error message points the wrong way: GPT-5.6 on Bedrock does not go through the Converse API. Send it a cachePoint and you get AccessDeniedException, saying you invoked an unsupported model or that your request does not permit prompt caching. That reads like the model cannot cache. It can — you have to use invoke-model with a Chat Completions body, where the breakpoint is called prompt_cache_breakpoint. It also rejects max_tokens and wants max_completion_tokens.
What it actually saves#
Knowing it hit is not the same as knowing what it is worth, so I modelled the shape an agent loop actually has: one 4,470-token stable prefix, five consecutive turns, only the trailing question changing. Same workload twice — once without a cachePoint, once with.
Without caching, all five turns are identical:
| Turn | inputTokens | Write | Read | Cost |
|---|---|---|---|---|
| 1 | 4470 | 0 | 0 | $0.013410 |
| 2 | 4470 | 0 | 0 | $0.013410 |
| 3 | 4470 | 0 | 0 | $0.013410 |
| 4 | 4470 | 0 | 0 | $0.013410 |
| 5 | 4470 | 0 | 0 | $0.013410 |
| Total | 22350 | 0 | 0 | $0.067050 |
With a cachePoint:
| Turn | inputTokens | Write | Read | Cost |
|---|---|---|---|---|
| 1 | 11 | 4459 | 0 | $0.016754 |
| 2 | 11 | 0 | 4459 | $0.001371 |
| 3 | 11 | 0 | 4459 | $0.001371 |
| 4 | 11 | 0 | 4459 | $0.001371 |
| 5 | 11 | 0 | 4459 | $0.001371 |
| Total | 55 | 4459 | 17836 | $0.022237 |
Five turns, 66.8% cheaper.
Look at the first row before celebrating. Turn one with caching is more expensive than turn one without — $0.016754 against $0.013410, roughly 25% more. That is the 1.25× write premium, paid in full, up front.
The saving lives in turn two onward: $0.013410 drops to $0.001371, about 90% off. The extra $0.0033 spent on turn one is recovered by turn two.
Which makes the economics of this genuinely simple: a prefix has to be reused at least twice before caching pays. Send it once and walk away and you have lost 25%. The more turns, the more the write amortises — five turns gave 67%, and ten would approach 90%.
If you want to run these numbers against your own prefix and turn count before writing any code, the LLM cost calculator does the same arithmetic at current prices.
A note on latency, since people expect it as a second benefit: the two five-turn runs took 12.29s and 11.47s. Not a meaningful difference. My outputs were deliberately tiny and a 4,470-token prefix is short, so there was no prefill advantage to see. Demonstrating the latency win needs a much longer prefix and realistic output lengths, and I did not do that here.
What I do now#
- Order the prompt so stable content leads, and put the
cachePointafter it, before user input. - Verify once, always: same prefix twice — expect a write, then a read.
- Send one more request with no breakpoint as a control, to prove those fields came from the cache.
- Cost with all three token fields summed, never off
inputTokens. - Before estimating any benefit, ask how many times this prefix gets reused. Under two, leave caching off.
- Do not copy threshold numbers out of documentation. Two requests and a few cents settle it.
- Alert on cache reads staying at zero. Whatever the threshold turns out to be, a sub-threshold prefix fails silently, and monitoring is the only thing that gives that failure a voice.
Closing#
Total spend across every call in this article was under $0.20, and it corrected two things I had been assuming.
The first was that enabling caching saves money. It does not, on its own — the first call is dearer, and whether you come out ahead depends entirely on how many times that prefix comes back.
The second was subtler, and it is really a lesson about testing. I had originally concluded “below 4,096 does not cache” from two points, 566 and 4,401, with the entire three thousand tokens between them untested. That sentence came out of the documentation; it was merely sitting next to measured data, which made it look measured. It is an easy mistake to make: using an experiment that cannot distinguish your hypotheses to “confirm” a number you already believe.
What finally settled it was not a longer prefix. It was the controls — running the same method against models whose answer was already known. If the method is broken, that is where it shows.
So when a number matters now I ask two questions first. If this number were wrong, would my test points reveal it? And if my method is wrong, what would tell me? The first is answered by adding points. The second by finding something with a known answer. Neither is clever, but both beat stopping at a result that looks agreeable.
Prices, APIs and thresholds all move. Measure yours before you depend on any figure here.
References
- Prompt caching for faster model inference (includes the per-model minimum token table) — AWS Bedrock documentation
- Amazon Bedrock pricing — AWS
- Prompt caching — Anthropic documentation
- Your Agentic Workflow's Cache Keepalive Costs 8x Too Much (v2: the interval frontier) — Maxim Khailo
Frequently asked
My prompt is 2,000 tokens. Can I use prompt caching with Claude Sonnet 4.5 on Bedrock?
Is the whole table wrong, or just that one row?
Does turning caching on always save money?
How do I confirm caching is actually working?
Why is my inputTokens value suddenly tiny?
Read next
- Tech Deep Dive
The Mysterious 400 in Claude Multi-Turn Chats: Debugging a Thinking Signature Corruption
A customer's Claude extended-thinking conversations kept failing with 400 Invalid signature — but only on some gateway channels. A full postmortem: what the signature mechanism is, how a proxy's JSON re-serialization silently corrupts base64, five controlled experiments that pinned it down, and a fix checklist for every LLM gateway developer.
- AI & Agent
Seedance 2.5 Hands-On: 725 Credits Buy 27 Seconds, and the CLI Costs Extra
A ¥69/month membership buys 725 credits — enough for 27 seconds of finished video, about ¥2.5 per second. The Dreamina CLI needs the ¥998/month premium tier, which is where my agent-driven pipeline stopped. Three blockers, one cost calculation done on what I actually paid, and four things wrong with the subtitles.
- AI & Agent
How Do I Explain "Agent" to My Wife?
From 'the Lark bot can't do math' to 'raising your own AI lobster' — an AI-agent explainer for normal humans. Thirteen burning questions covering LLMs, tokens, Tools, MCP, RAG, Skills, Memory, Multi-Agent systems, and 2026 model prices. The AI isn't dumb — it just hasn't been raised properly yet.
- AI & Agent
How to Build a RAG System with LangChain and Elasticsearch
A hands-on guide to building Retrieval Augmented Generation — from vector embeddings to context-enhanced LLM answers.
Get new posts by email
One email when a new article goes up. No ads, unsubscribe in one click.
Your address is used for post notifications only.
Discussion
Comments are GitHub Discussions on this repository; sign in with GitHub to post.