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.

zhuermu · · 12 min

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.

Prefill produces the KV cache; a cache hit skips that whole phase and pays roughly a tenth for the read

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.

Change the system block and everything after it is voided, recomputed, and rebilled at write price

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 toolssystemmessages, 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.

Bisecting the crossover: 999 does not cache, 1,054 does, and 2,421 and 3,741 cache despite sitting below the documented 4,096

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.

ModelDocumented minimumHighest not cachedLowest cachedVerdict
Claude Sonnet 4.54,0969991,054Docs 4× too high
Claude Sonnet 4.61,0241,0001,055Matches
Claude Sonnet 5not listed9831,055Measured 1,024
GPT-5.6 Terra1,0248171,504Matches

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:

TurninputTokensWriteReadCost
1447000$0.013410
2447000$0.013410
3447000$0.013410
4447000$0.013410
5447000$0.013410
Total2235000$0.067050

With a cachePoint:

TurninputTokensWriteReadCost
11144590$0.016754
21104459$0.001371
31104459$0.001371
41104459$0.001371
51104459$0.001371
Total55445917836$0.022237

Five turns, 66.8% cheaper.

Turn one is 25% more expensive with caching; the lines cross at turn two and diverge from there

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 cachePoint after 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

  1. Prompt caching for faster model inference (includes the per-model minimum token table) — AWS Bedrock documentation
  2. Amazon Bedrock pricing — AWS
  3. Prompt caching — Anthropic documentation
  4. 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?
Yes. AWS documents a 4,096-token minimum for that model, but I measured writes and hits at 1,054 tokens. The real threshold is 1,024 — the same as Anthropic's first-party API. That line in the documentation is wrong.
Is the whole table wrong, or just that one row?
Just that row. I measured Claude Sonnet 4.6, Claude Sonnet 5 and GPT-5.6 Terra with the same harness, and all three agreed with their documented figure of 1,024. Only Sonnet 4.5's 4,096 was off, and it was off by 4×.
Does turning caching on always save money?
No. The first call is more expensive, because a cache write is billed at 1.25× the normal input rate. In my five-turn run the first turn cost $0.016754 with caching against $0.013410 without — about 25% more. You break even on the second reuse, so a prefix you send only once is a straight loss.
How do I confirm caching is actually working?
Send the same prefix twice and read cacheWriteInputTokens and cacheReadInputTokens in the response usage. The first call should show a write, the second a read. If both are zero the prefix never cached — and the API will not tell you, because the request still returns 200.
Why is my inputTokens value suddenly tiny?
Because with caching on, inputTokens only counts the part after the cache point. My 4,470-token request reported inputTokens of 11 on a hit. Total input is inputTokens + cacheReadInputTokens + cacheWriteInputTokens. Costing off inputTokens alone is wrong by an order of magnitude.
Share this article X LinkedIn Hacker News Reddit

Read next

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.