Guide · AI engineering

Prompt caching, and the traffic pattern where it costs you more

Prompt caching can cut the input cost of a repeated large prompt by around 87% and remove most of the time-to-first-token. It can also make your bill 24% worse, and the difference is entirely down to how often your requests arrive and what order your prompt is assembled in. Here is how prefix caching works, the arithmetic that decides whether it pays, and the mistakes that silently produce a 0% hit rate.


What is actually being cached

Before a model generates its first output token it must process every input token — the prefill phase. That work produces a large intermediate structure, the key-value cache, holding one key and one value vector per token per layer. For a 20,000-token prompt that is a substantial amount of computation and it is identical every time the prompt is identical.

Prompt caching stores that intermediate state on the provider's side and reuses it when a later request begins with the same tokens. You are not caching the response — same prompt, different sampling, still a different answer. You are caching the prefill work, which means you get both the cost reduction and the latency reduction, and the latency reduction is often the part users actually notice.

It is a prefix match, and that is the whole design

This is the one rule everything else follows from: the match starts at token zero and runs forward until the first difference. Everything after that difference is a miss. It is not a substring search, not a fuzzy match, and not per-block.

Request A: [system 4,000 tok][docs 16,000 tok][question 500 tok] Request B: [system 4,000 tok][docs 16,000 tok][different question] └──────── 20,000 tokens shared ────────┘ ← cache hit Request C: ["Today is 2026-07-31."][system][docs][question] └── one changed token at position 0 ──┘ ← 0% hit rate

Request C is the mistake that destroys more caching implementations than everything else combined. A timestamp, a request UUID, or a user's name interpolated into the top of a system prompt changes byte zero of the prefix on every single call, and nothing after it can ever be reused. The prompt looks 99.9% identical. The cache hit rate is exactly zero.

The corollary is an ordering rule you can apply mechanically: assemble the prompt in descending order of stability. Tool definitions and the frozen system prompt first, then static documents and few-shot examples, then conversation history, then the current user turn. Anything that varies per request goes last, always.

TTL: the window you have to hit

Cache entries expire. The standard window is five minutes from last use — each hit refreshes it — and providers typically also offer a longer one-hour option at a higher write cost. Five minutes is short enough that the traffic pattern of your application, not the size of your prompt, is what decides whether caching helps.

Two applications with identical prompts can land on opposite sides of this. A chat interface where a user sends a follow-up every thirty seconds hits the cache on every turn after the first. A weekly report generator that fires one request at a time, ten minutes apart, misses on every single one and pays the write premium each time for a cache nothing ever reads.

The cost arithmetic

Caching is not free to populate. Writing an entry costs more than a normal input token, and reading one costs dramatically less. The multipliers, relative to the base input price:

OperationCost vs. base inputWhen it happens
Cache write, 5-minute TTL1.25×First request with a new prefix, or any request after expiry.
Cache write, 1-hour TTLSame, with the longer window.
Cache read~0.1×Every subsequent request that matches the prefix within the TTL.
Uncached inputEverything after the last cached breakpoint.
Output tokens1× (unaffected)Always. Caching does nothing for output cost, ever.

From those two multipliers you can derive the break-even directly. For N requests sharing one prefix within the TTL window, with W tokens in the shared prefix:

cached cost = 1.25·W + 0.1·W·(N−1) uncached cost = N·W set equal: 1.25 + 0.1(N−1) = N 1.15 = 0.9N N = 1.28 5-minute TTL: caching pays from the SECOND request onward. 1-hour TTL (2× write): break-even at N = 2.11 — you need a THIRD.

That is a low bar and it is why caching is close to free money in the common case. The trap is that N counts requests within one TTL window, not requests in total. Two hundred requests spread evenly across a day is N = 1 for every window.

A worked example, both directions

Take a support assistant: a 20,000-token stable prefix (system instructions, tool definitions, a product knowledge base) plus a 500-token variable user question. Two hundred requests in an hour.

BUSY HOUR — 200 requests, ~18 seconds apart, all inside the 5-min TTL No caching: 200 × 20,500 = 4,100,000 effective tokens With caching: 1 × 20,000 × 1.25 = 25,000 199 × 20,000 × 0.1 = 398,000 200 × 500 × 1.0 = 100,000 ───────── 523,000 effective tokens Reduction: 87.2%. At a $3-per-million input rate: $12.30 -> $1.57 per hour.

Now the same 200 requests, same prompt, spread evenly across a 24-hour day — one every 7.2 minutes, which is longer than the five-minute TTL. Every request finds an expired entry, writes a fresh one, and nothing ever reads it:

SPARSE DAY — 200 requests, 7.2 minutes apart, every one a cold write No caching: 200 × 20,500 = 4,100,000 effective tokens With caching: 200 × 20,000 × 1.25 = 5,000,000 200 × 500 × 1.0 = 100,000 ───────── 5,100,000 effective tokens 24% MORE EXPENSIVE than not caching at all.

Same prompt, same request count, same code — the only variable was arrival timing, and it moved the result from an 87% saving to a 24% penalty. This is why "turn on caching everywhere" is bad advice and why the hit rate is the only number that tells you which regime you are in.

When caching does nothing, or worse

  • The prefix is below the minimum cacheable length. Providers enforce a floor, commonly in the 512 to 4,096 token range depending on the model, and it is not monotonic across model generations. Below it, the cache marker is silently ignored — no error, no entry, and a diagnostic field showing zero tokens written.
  • Something varies at the front. A timestamp, a UUID, a session ID, a user's name in the system prompt. Byte zero differs, everything after it misses.
  • Traffic is sparser than the TTL. The case above. Either batch requests together in time or switch to the longer TTL and accept the higher write cost.
  • Tool definitions changed. Tools usually render at the very front of the prompt, so adding, removing, or reordering one invalidates everything. Serialise them deterministically — sort by name — because an unordered dict or set will reorder itself between runs and you will never see why.
  • Non-deterministic JSON serialisation. json.dumps without sort_keys=True, or anything iterating a set, produces a byte-different prefix from identical data.
  • You switched models. Caches are model-scoped. Routing between models mid-conversation means each one maintains its own entry and neither is warm.
  • Concurrent identical requests. An entry only becomes readable once the first response begins streaming. Fire ten parallel requests with the same prefix and all ten pay the write. Send one, wait for its first token, then fan out the rest.
  • You expected it to reduce output cost. It does not. Output tokens are billed at full price regardless, and on a short-prompt, long-answer workload caching is nearly pointless.

Measure it or you do not know

Every provider that offers caching reports it in the response usage object — typically a count of tokens written to cache, a count read from cache, and a count processed uncached. Log all three on every call and compute a hit rate. This is not optional instrumentation; a caching implementation you are not measuring is one you cannot distinguish from a broken one, and the broken version costs 25% more while looking identical in your code.

hit_rate = cache_read_tokens / (cache_read_tokens + cache_write_tokens + uncached_tokens) > 0.8 working as intended 0.3-0.8 partial — check whether traffic is sparser than the TTL ~ 0.0 something varies in the prefix, or it is below the minimum length # total prompt size = uncached + cache_creation + cache_read # an agent showing 4k "input tokens" after hours of work is not small — # the rest was served from cache. Sum the three, never read one.

If the hit rate is zero across repeated calls you believe are identical, do not theorise — diff the rendered prompts. Serialise the exact bytes sent on two consecutive requests to files and run a diff. The culprit is always visible and it is always something small and boring.

Architectural moves that raise the hit rate

  • Freeze the system prompt. Never interpolate the date, the user, or a mode flag into it. Inject that context later in the message list, where invalidating from that point costs almost nothing.
  • Batch by prefix. If you have a queue of jobs sharing a large prefix, sort the queue by prefix and run each group together. This is the single change that converts the sparse-traffic penalty into the busy-hour saving.
  • Do not swap tools mid-conversation. Tools sit at position zero. If you need modes, pass the mode as message content rather than changing which tools exist.
  • Make forked calls reuse the parent prefix exactly. Summarisation passes, sub-agents, and side computations often rebuild the system prompt from scratch and miss the cache they were sitting on. Copy the parent's prefix verbatim and append.
  • Pre-warm only when the first request's latency is user-visible. A startup warm-up call is a real write charge; it is worth it for an interactive surface after a deploy and it is waste for a background job.
  • Consider the longer TTL for bursty traffic. Gaps longer than five minutes but shorter than an hour are exactly the case the one-hour window exists for — at 2× write cost, so you need three hits per window instead of two.

The local equivalent

Local runtimes do the same thing under a different name. llama.cpp and Ollama keep the KV cache in memory between requests and reuse the matching prefix automatically, which is why a follow-up question in the same session starts generating almost immediately while a fresh session with the same context takes seconds. The mechanism is identical; the economics are not, because locally you are saving wall-clock time rather than money.

That makes the ordering discipline more important locally, not less, since prefill on Apple Silicon runs at a few hundred tokens per second and a 20,000-token prompt reprocessed from scratch is a genuinely long wait. Same rule, same reason: stable content first, volatile content last. Between that and retrieving fewer, better chunks — which is what a properly evaluated retrieval pipeline gives you — you can usually take a local RAG setup from unusable to pleasant without touching the model at all.

The short version

  1. Caching stores prefill work, keyed on an exact prefix match from token zero.
  2. Order the prompt stable-first: tools, system, static documents, history, then the current turn.
  3. Writes cost about 1.25× base input on the short TTL; reads cost about 0.1×.
  4. It pays from the second hit within the TTL window, and it costs you 24% more if you never get one.
  5. Log the three usage counters and track a hit rate. Zero means something varies in your prefix.
  6. It never reduces output cost. If your prompts are short and your answers long, this is not your lever.

Tools referenced in this guide


FAQ

Quick answers

How does prompt caching actually work?

The provider stores the key-value state produced while processing your prompt's input tokens, then reuses it when a later request begins with the identical token sequence. You are caching the prefill computation, not the response, so you save both input cost and time-to-first-token while still getting a freshly generated answer each time.

Why is my prompt cache hit rate zero?

Almost always because something varies at the front of the prompt. A timestamp, a request UUID, a session ID, or a user's name interpolated into the system prompt changes the prefix at position zero, and since matching runs forward from token zero until the first difference, nothing after it can be reused. Diff the exact bytes of two consecutive requests and the culprit is always visible.

How long does a prompt cache entry last?

The standard window is about five minutes from last use, refreshed on each hit, with a longer one-hour option available at roughly double the write cost. Five minutes is short enough that your request arrival pattern, not your prompt size, determines whether caching helps at all.

How many requests do I need before prompt caching pays off?

With the five-minute window, caching pays from the second matching request onward: a write costs about 1.25 times base input and a read about 0.1 times, so two requests cost 1.35 units versus 2 uncached. With the one-hour window the write costs about 2 times, pushing break-even to the third request. The critical detail is that these counts are per TTL window, not lifetime totals.

Can prompt caching ever cost more than not caching?

Yes, and it is a common failure. If your requests arrive further apart than the TTL, every one writes a fresh entry that nothing ever reads, and you pay the write premium each time. Two hundred requests spaced 7.2 minutes apart against a five-minute TTL comes out roughly 24% more expensive than not caching at all.

Does prompt caching reduce output token cost?

No. Caching only affects the input side by skipping prefill work on the matched prefix. Output tokens are billed at full price regardless of hit rate, so a workload with short prompts and long generated answers gets almost nothing from caching no matter how it is configured.

How should I order a prompt to maximize cache hits?

Descending order of stability. Tool definitions and a frozen system prompt first, then static documents and few-shot examples, then conversation history, and the current user turn last. Anything that changes per request belongs at the very end, so that changing it invalidates as little of the prefix as possible.

Why do parallel requests with the same prompt all miss the cache?

A cache entry only becomes readable once the first response starts streaming, so simultaneous requests are all writing rather than reading. Send one request, wait for its first token, then fan out the rest — they will read the entry the first one just wrote instead of each paying full price.