Back to the map
Language ModelsAdvanced9h

Prompt caching

Stop paying twice for the same prefix.

Most calls to a model repeat a great deal of text: the same instructions, the same examples, the same reference documents, followed by a small changing question. Prompt caching lets the provider keep the processed form of that unchanging opening stretch, so it does not have to be worked through again. Send the same opening twice and the second call reuses it, at a fraction of the price and noticeably faster.

On a production system that sends a long fixed preamble every time, this is usually the largest single reduction available on the bill, and it cuts the wait before the first word as well. The catch is that it matches from the very start of the prompt forward, so one changed character early — a timestamp, a request id, the user's name in the system prompt — throws away everything after it. You pay full price, and nothing errors to tell you.

It builds on context windows and on tokenization, since what gets cached and matched is tokens. It changes how you lay a prompt out: stable content first, volatile content last, which is often the opposite of how prompts get written. It pairs with streaming as the other half of cost and latency work, and with context management, which pulls the other way by rewriting the history caching wants left alone.

The cache is matched from the start of the prompt forward, so anything that changes belongs at the end.

In more detailProviders can cache the processed form of a stable prompt prefix, so repeated calls sharing that prefix cost far less. It is prefix-matched, which means a single changed byte anywhere early invalidates everything after it. The design consequence is to put stable content first and volatile content, timestamps, per-request ids, last.

Learn first

Section 1 of 2, 5h

Prompt caching

By the end: You can cut repeated-prompt cost substantially and tell whether it is working.

  • What is cached and why it is cheaper
  • Prefix matching, and what invalidates it
  • Ordering a prompt for caching
  • Verifying it from usage data

A model re-reads your entire prompt on every request. If the first twenty thousand tokens are the same every time — a system prompt, a tool schema, a document the conversation is about — you are paying to process identical text repeatedly. Prompt caching lets the provider keep the computed state for that prefix and skip the work, which is cheaper and faster. This section covers what is actually cached, the one rule that decides whether you get a hit, how to order a prompt so you do, the arithmetic of whether it pays, and how to check from the response rather than by hoping.

What is cached, and why it is cheaper#

What is stored is not the text but the model's internal state after reading it — the key and value tensors for those tokens, the same KV cache that makes generation fast. Because attention is causal, every token attends only to what came before it, so that state depends on the prefix alone and is valid for any continuation. That is the whole mechanism, and it explains the constraint that follows: the cache works from the start of the prompt forward, and only for an exact match.

The pricing follows the work. Writing the prefix into the cache costs somewhat more than processing it normally, because the state has to be stored as well as computed. Reading it back costs a small fraction, because nothing is recomputed. Providers differ in the exact multipliers and in how long an entry survives, so check the ones you are billed under, but the shape is the same everywhere.

Prefix matching, and what invalidates it#

The match is on an exact prefix, byte for byte, from the very first token. Change anything early and everything after it is a miss, regardless of how much of the rest is identical. This is the single fact that determines whether caching works for you, and it has one consequence people discover the expensive way.

prompt                                          cached?
[system 20k][doc][question A]                   write
[system 20k][doc][question B]                   hit on the 20k + doc
[system 20k + today's date][doc][question C]    MISS on everything
[question D][system 20k][doc]                   MISS on everything
The third row is the trap: one variable token near the front invalidates every cached token after it. A timestamp, a user id, a randomised greeting -- anything that changes per request belongs after the stable material, not before it.

The date in the system prompt

Putting the current date at the top of a system prompt is a common and reasonable-looking habit, and it silently costs you every cache hit you would otherwise have had. If the model needs the date, put it at the end of the prompt, after the cached material, or in the user turn. The same applies to a session id, a user name, or anything else that is stable within a request and different between them.

Ordering a prompt for caching#

The rule follows directly: arrange the prompt from least variable to most. Everything identical across all requests first, then material shared within a session, then the part that changes every time.

1. tool definitions            identical for the lifetime of the deployment
2. system instructions         identical until you deploy a change
3. few-shot examples           identical, and often large
4. retrieved documents         identical within a conversation
5. conversation history        grows, but only at the end
6. the current user message    different every time
7. the current date, ids       different every time
Conversation history sits at 5 for a reason: appending to the end preserves the prefix, so each turn re-reads the whole conversation from cache and only pays full price for the new turn.

That last point is what makes caching so effective for agents in particular. An agent loop appends a tool call and its result to the end of a growing message list, so every iteration is a pure extension of the previous prefix. Handled correctly, a twenty-step agent pays full price for its context once and a tenth of it nineteen times. Handled carelessly — by rewriting or re-summarising the history in place — it pays full price twenty times for the same tokens.

Working out whether it pays#

Caching is not free on the first request, so for genuinely one-off prompts it is a loss. The break-even arrives quickly, and the arithmetic is worth doing on your own numbers rather than assuming either way.

BASE, WRITE, READ = 1.0, 1.25, 0.10     # multipliers on a normal input token

def cost(prefix, suffix, requests, cached):
    """Total input cost in units of one normal input token."""
    if not cached:
        return requests * (prefix + suffix) * BASE
    first = prefix * WRITE + suffix * BASE
    rest = (requests - 1) * (prefix * READ + suffix * BASE)
    return first + rest

prefix, suffix = 20_000, 500
for n in (1, 2, 3, 10, 100, 1000):
    u, c = cost(prefix, suffix, n, False), cost(prefix, suffix, n, True)
    print(f"{n:>5} requests  uncached {u:>10,.0f}  cached {c:>9,.0f}  {1 - c/u:>7.1%}")

#     1 requests  uncached     20,500  cached    25,500   -24.4%   <- a loss
#     2 requests  uncached     41,000  cached    28,000    31.7%
#     3 requests  uncached     61,500  cached    30,500    50.4%
#    10 requests  uncached    205,000  cached    48,000    76.6%
#   100 requests  uncached  2,050,000  cached   273,000    86.7%
#  1000 requests  uncached 20,500,000  cached 2,523,000    87.7%
One request costs a quarter more; from the second onward it pays, and at volume it approaches the read multiplier, which is a ninety per cent saving on the cached portion. Substitute your provider's multipliers -- they are the only inputs that matter.

The number that decides it in practice is not the request count but whether the second request arrives before the entry expires. Cache lifetimes are short — commonly a few minutes, extendable at extra cost — so a prefix reused twenty times an hour caches beautifully and the same prefix used twice a day never hits at all. Work out your reuse rate within the lifetime, not in total.

Verifying it from usage data#

Never assume a cache is working. Every provider reports cache reads and writes in the usage block of the response, and those fields are the only evidence that matters — a prompt you believe is stable is often not, and the fields say so immediately.

usage:
  input_tokens                    512     paid at the normal rate
  cache_creation_input_tokens  20,000     written this request
  cache_read_input_tokens           0     read from cache

next request, if all went well:
  input_tokens                    498
  cache_creation_input_tokens       0
  cache_read_input_tokens      20,000     the prefix was reused
Field names vary by provider; the three quantities do not. If creation is non-zero on every request, your prefix is not stable and something near the front is changing.

Log those three numbers per request from the start. It turns cache behaviour into something you can graph and alert on, and the usual discovery is that a deploy changed a system prompt, or that one code path assembles the prompt in a different order from the rest — both of which show up as a cache hit rate falling off a cliff, and neither of which shows up any other way.

Where people go wrong

Putting personalisation at the top of the system prompt. Opening with the user's name feels natural and gives every single user a completely separate cache, which is the same as having none.

Build this: Take a prompt with a long system message, send it twenty times, and record the cached token count. Then add a timestamp to the top and watch it drop to zero.

Open this section on its own page
Section 2 of 2, 4h

The cost maths, and caching whole responses

By the end: You can work out whether caching pays for a given traffic pattern, and cache responses without serving wrong answers.

  • Cache writes, cache reads and expiry
  • Working out whether it pays
  • Exact response caching
  • Semantic caching and its risks
  • What must be in a cache key

Prefix caching is not free, and whether it saves you anything depends on your traffic rather than your prompt. Writing a prefix into the cache costs more than plain input, reading it back costs much less, and the entry expires after minutes without use. With steady traffic that is a large saving; with one request an hour it is a loss. This section is that arithmetic, and then caching at the other end — storing whole responses, where an exact key is safe and a semantic one can serve a confidently wrong answer to a question that merely resembled an earlier one.

Writes, reads and expiry#

Prefix caching is not free on every provider. On some, writing a prefix into the cache costs a little more than normal input, and reading it back costs much less. Cached prefixes also expire after a period without use, often minutes, and providers set a minimum prefix length. Look up the current terms for your provider; they change.

Does it pay? Work it out#

Illustrative prices per million input tokens (check your provider):
  normal input   $3.00
  cache write    $3.75
  cache read     $0.30

Prefix of 8,000 tokens, 1,000 requests in the cache lifetime:
  without cache  1,000 x 8,000 x $3.00 / 1M          = $24.00
  with cache     1 write:  8,000 x $3.75 / 1M        = $0.03
                 999 reads: 999 x 8,000 x $0.30 / 1M = $2.40
                                                total = about $2.43

With steady traffic it is a large saving. With one request every hour and a cache that expires in minutes, every request pays the write price and caching costs more than not caching. Traffic pattern decides.

The variable is time, not the request count#

The prompt-caching section works out break-even in requests and finds it at the second one. That answer is only right if the second request arrives before the entry expires, and whether it does is a property of your traffic rather than your prompt. Model the arrivals as a Poisson process with a mean gap and the chance that the previous request is still cached is one minus the exponential of minus the lifetime over the gap. That gives an expected cost per request you can put a number on.

import math

BASE, WRITE, READ = 1.0, 1.25, 0.10      # multipliers on a normal input token
TTL = 300                                # seconds an unused entry survives

def effective(gap, ttl=TTL):
    """Poisson arrivals: the chance the previous request is still cached."""
    p_hit = 1 - math.exp(-ttl / gap)
    return p_hit, p_hit * READ + (1 - p_hit) * WRITE

print(f"{'mean gap':>10}{'hit rate':>10}{'cost/req':>10}{'vs no cache':>13}")
for gap in (5, 30, 120, 300, 900, 1800, 3600):
    p, c = effective(gap)
    print(f"{gap:>9}s{p:>10.0%}{c:>10.2f}{1 - c / BASE:>12.0%}")

p_break = (WRITE - BASE) / (WRITE - READ)
gap_break = -TTL / math.log(1 - p_break)
print(f"break-even hit rate {p_break:.1%}, reached at a mean gap of {gap_break:.0f}s")

#   mean gap  hit rate  cost/req  vs no cache
#         5s      100%      0.10         90%
#        30s      100%      0.10         90%
#       120s       92%      0.19         81%
#       300s       63%      0.52         48%
#       900s       28%      0.92          8%
#      1800s       15%      1.07         -7%
#      3600s        8%      1.16        -16%
# break-even hit rate 21.7%, reached at a mean gap of 1224s
Read the last two rows: at a request every half hour against a five-minute lifetime, caching costs 7% more than not caching, and at one an hour it costs 16% more. The break-even gap works out at almost exactly four times the lifetime, which is the rule of thumb worth keeping.

That break-even hit rate falls straight out of the multipliers rather than out of the traffic: a write costs a quarter more than plain input and a read costs a tenth of it, so a hit has to happen often enough to repay the extra quarter, which is 0.25 divided by 1.15, or about 22%. Substitute your provider's numbers and the shape holds. It also tells you what to do about a prefix that is used too rarely — batch the work so requests arrive together, or pay for a longer lifetime where the provider sells one, and compare that price against the 25% write premium you are paying repeatedly instead.

Caching the whole response#

If the exact same request arrives again, you can return the stored answer and skip the model entirely. This suits repeated, non-personal questions and deterministic pipelines. The key must include everything that affects the answer.

import hashlib, json

def cache_key(model, prompt_version, messages, settings, tenant_id):
    raw = json.dumps([model, prompt_version, messages, settings, tenant_id], sort_keys=True)
    return hashlib.sha256(raw.encode()).hexdigest()

a = {"temperature": 0.0, "max_tokens": 512}
b = {"max_tokens": 512, "temperature": 0.0}      # the same settings, built elsewhere
msgs = [{"role": "user", "content": "What is the refund window?"}]

print("same key with sort_keys:  ",
      cache_key("m-1", "v3", msgs, a, "acme")[:12],
      cache_key("m-1", "v3", msgs, b, "acme")[:12])
print("without sort_keys:        ",
      hashlib.sha256(json.dumps(a).encode()).hexdigest()[:12],
      hashlib.sha256(json.dumps(b).encode()).hexdigest()[:12])
print("different tenant:         ",
      cache_key("m-1", "v3", msgs, a, "acme")[:12],
      cache_key("m-1", "v3", msgs, a, "globex")[:12])

# same key with sort_keys:   5d721c1d80c1 5d721c1d80c1
# without sort_keys:         0f6fb34da972 a78cfe8d527c
# different tenant:          5d721c1d80c1 b19d2c710f32
The middle line is a whole afternoon of confusion: two dictionaries that compare equal in Python serialise to different bytes and hash to different keys, so the cache has a hit rate of zero and no error anywhere. sort_keys is the fix, and a hit-rate metric is how you notice.
  • The model id, including the dated version. A new model returns a different answer to the same prompt.
  • The prompt version, bumped on every deploy that touches the template. Otherwise yesterday's wording is served all week.
  • Every message, in full, and the sampling settings. Temperature above zero means the stored answer is one sample of many, which is a decision to make deliberately.
  • The tenant or user, whenever the answer could differ between them. This is the field whose absence becomes an incident rather than a bug.
  • A version of the underlying data, if the answer depends on retrieved documents. Changing a document has to invalidate the answers built from it.

An exact cache is safe when the same key genuinely implies the same correct answer, and that is a claim about your application rather than about caching. It holds for a documentation assistant answering the same five hundred questions all day. It does not hold for anything that reads live state, anything personalised, or anything where yesterday's answer is stale rather than merely old, and for those cases a short time to live is not a fix but a smaller version of the same bug.

Semantic caching is riskier than it looks#

A semantic cache returns a stored answer when a new question is similar enough to an old one, judged by embeddings. The danger is that similar is not the same. 'Cancel my order' and 'do not cancel my order' are close in embedding space. Answers that depend on who is asking can leak between users.

That is easy to say and easy to dismiss, so measure it. Take five cached questions, five genuine paraphrases that should hit them, and six near-misses that must not — the annual plan rather than the monthly one, the paid tier rather than the free one, a negated version of the same sentence. Then sweep the threshold and count, at each one, how many paraphrases hit the right entry and how many near-misses hit anything at all.

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

CACHED = {
    "How do I cancel my subscription?": "Open Billing and choose Cancel plan.",
    "What is the refund window on the monthly plan?": "Fourteen days, in full.",
    "How do I reset my password?": "Use Forgot password on the sign-in page.",
    "Is there a discount for annual billing?": "Yes, two months free.",
    "What is the rate limit on the free tier?": "Sixty requests per minute.",
}
PARAPHRASE = {
    "How can I cancel my subscription?": "How do I cancel my subscription?",
    "Where do I reset my password?": "How do I reset my password?",
    "What is the refund period for monthly plans?": "What is the refund window on the monthly plan?",
    "Do you give a discount if I pay annually?": "Is there a discount for annual billing?",
    "How many requests per minute does the free tier allow?": "What is the rate limit on the free tier?",
}
NEAR_MISS = [
    "What is the refund window on the annual plan?",
    "What is the rate limit on the paid tier?",
    "I do not want to cancel my subscription, how do I keep it?",
    "How do I reset another user's password?",
    "Is there a discount for annual billing for non-profits?",
    "How do I cancel my free trial before it becomes a subscription?",
]

keys = list(CACHED)
vec = TfidfVectorizer().fit(keys + list(PARAPHRASE) + NEAR_MISS)   # stands in for embeddings
K = vec.transform(keys)

def nearest(question):
    sims = cosine_similarity(vec.transform([question]), K)[0]
    i = int(sims.argmax())
    return keys[i], float(sims[i])

print(f"{'threshold':>10}{'true hits':>11}{'wrong hits':>12}{'misses':>8}")
for t in [0.30, 0.40, 0.50, 0.60, 0.70, 0.80, 0.90]:
    hits = sum(nearest(q)[1] >= t and nearest(q)[0] == want for q, want in PARAPHRASE.items())
    wrong = sum(nearest(q)[1] >= t for q in NEAR_MISS)
    print(f"{t:>10.2f}{hits:>11}{wrong:>12}{len(PARAPHRASE) - hits:>8}")

right = [nearest(q)[1] for q, want in PARAPHRASE.items() if nearest(q)[0] == want]
wrong = [nearest(q)[1] for q in NEAR_MISS]
print(f"lowest correct paraphrase {min(right):.2f}, highest near-miss {max(wrong):.2f}")

for q in sorted(NEAR_MISS, key=lambda q: -nearest(q)[1])[:3]:
    match, sim = nearest(q)
    print(f"{sim:.2f}  {q}")
    print(f"      served: {CACHED[match]}")

#  threshold  true hits  wrong hits  misses
#       0.30          3           6       2
#       0.40          3           6       2
#       0.50          3           6       2
#       0.60          2           4       3
#       0.70          2           3       3
#       0.80          0           3       5
#       0.90          0           0       5
# lowest correct paraphrase 0.13, highest near-miss 0.88
# 0.88  What is the refund window on the annual plan?
#       served: Fourteen days, in full.
# 0.86  What is the rate limit on the paid tier?
#       served: Sixty requests per minute.
# 0.82  Is there a discount for annual billing for non-profits?
#       served: Yes, two months free.
TF-IDF stands in for an embedding model so the run is reproducible without a network call, and it flatters nobody: the lowest true paraphrase scores 0.13 while the highest near-miss scores 0.88, so the two populations do not separate at any threshold. A real embedding model lifts the paraphrase column and leaves the near-miss column where it is, because a plan name and a tier name are exactly the kind of one-word difference embeddings are built to ignore.

Read the three worst rows as answers rather than as numbers. A customer on the annual plan is told the refund window is fourteen days. A customer on the paid tier is told their rate limit is sixty requests a minute. Both answers are fluent, confident, cached and wrong, and neither raises an error anywhere in your system — which is the difference between a semantic cache and every other cache you have ever run.

  • Use it only for questions whose answer does not depend on the user.
  • Set the similarity threshold from an eval of near-miss pairs, not from intuition.
  • Scope the cache by tenant, and expire entries when the underlying data changes.
  • Build the near-miss set out of the entities your answers turn on — plan names, tiers, regions, dates — because those are the words a single embedding will not keep apart.
  • Log every semantic hit with both questions and the similarity. It is the only record that lets you find out afterwards what was served and to whom.

The cheap version of a semantic cache

Most of the saving people want from a semantic cache is available from an exact cache over a normalised question: lowercase, strip punctuation and whitespace, and hash that. It catches the repeats that actually dominate support traffic, which are the same question typed again rather than a clever paraphrase, and it cannot serve the annual-plan customer the monthly-plan answer. Try that first, measure the hit rate, and only reach for embeddings if the number is disappointing and you are prepared to run the threshold sweep above every time the corpus changes.

Where people go wrong

Adding a semantic cache to a support bot and discovering it answered one customer's question with another customer's account details.

Build this: Take your own traffic, or simulate a day of it, and compute cost with and without prefix caching using your provider's current prices. Then add an exact response cache to one endpoint and measure the hit rate over a week.

Open this section on its own page

Where you use this

A topic is easier to care about when you can see what it is for. These projects lean on it at a named stage.

Serve a model, then make it cheaper
  • Stage 5, Caching: What reusing a KV prefix actually skips, and the prefix-match rule that decides whether it happens.

Now actually do it

Reading a topic is not learning it. These are the hours that count, and each one tells you when you are finished.

1Prove caching is working, then break it

3h

Send the same long system prompt 20 times and record cached token counts. Then add a timestamp to the top and watch them drop to zero.

Done when: You have the before and after numbers and can spot a broken cache from usage data alone.

2Reorder a real prompt for caching

2h

Take a prompt with personalisation near the front. Reorder from stable to volatile and measure the cost difference across a hundred calls.

Done when: You can state the saving in dollars per thousand calls.

5 hours of practice

Words used here

1 term this topic teaches, each with a one-line definition if one of them stopped you.

Prompt caching
Reusing the computed KV cache for a prompt prefix that has not changed, so a long fixed preamble is processed once and skipped thereafter. Large savings in latency and cost for anything with a stable system prompt.

Interview questions

6 questions, grouped by the section that teaches the answer. An answer you have read is not an answer you can give, so use Test yourself and say it out loud first.

Prompt caching

What is prompt caching and when does it help?

Providers cache the processed form of a stable prompt prefix and charge much less to reuse it. It helps whenever calls share a long fixed preamble: a large system prompt, tool definitions, few-shot examples, or a document queried repeatedly.

What breaks prompt caching?

Any change in the prefix, since matching runs from the start and stops at the first difference. Timestamps, request ids, a user name near the top, or JSON serialised with unstable key order all invalidate it silently.

How do you verify caching is working?

Check the cached token count in usage data on repeated similar requests. Consistently zero means something volatile is in the prefix. It is worth asserting on in a test, because the failure has no behavioural symptom.

The cost maths, and caching whole responses

When does prompt caching not save money?

When requests are too infrequent for the cache to stay warm, so most requests pay the write price, or when the prompt changes near the start so the prefix rarely matches. Work out the numbers from your real traffic before assuming a saving.

Then they push further

How would you confirm the saving in production?

Read the cached and uncached token counts the API reports for each request, compute the hit rate and the effective price per request, and compare with the month before.

What gets marked up. Strong answers reason from traffic pattern and prices, not from 'caching is always cheaper'.

What are the risks of semantic caching?

Returning an answer to a question that is similar but means something different, and leaking answers between users when responses are personal. Limit it to non-personal questions, tune the threshold on near-miss examples, and scope it by tenant.

Then they push further

What would be in your cache key for an exact response cache?

The model and version, the prompt version, the full messages, the sampling settings, and the tenant or user when answers can differ between them. Leaving one out means serving an answer produced under different conditions.

Your cached prefix has a five-minute lifetime. How often do requests have to arrive before caching pays?

Break-even is a hit rate, and it comes from the multipliers: a write costs about 25% more than plain input and a read a tenth of it, so a hit has to repay the extra quarter, which is 0.25 over 1.15, or roughly 22%. Treating arrivals as Poisson, that hit rate is reached when the mean gap is about four times the lifetime, so with five minutes you need a request roughly every twenty minutes. Rarer than that and caching costs more than not caching.

Then they push further

It is rarer than that and you cannot change the traffic. What now?

Make the requests arrive together rather than spread out, by batching the work, so one write serves several reads. Or buy a longer lifetime if the provider sells one and compare its price against the write premium you are paying repeatedly. Or accept that this prefix should not be cached and spend the effort on a prefix that is.

What gets marked up. Strong answers reason in hit rate against the lifetime rather than in total request count, and know that a cache can be a net loss rather than merely ineffective.

These questions on their own page, one link per question

Resources

Free to watch and read, picked for this topic.

For the cost maths, and caching whole responses

Stop 14 of 50 on the AI Engineer route

0 of 50 topics done · 0h behind you, 603h to go

Next on this routeTool callingThe model asks; your code acts.7h

What you have marked done is stored in this browser only. It does not reach a server, and clearing site data will clear it.