DeepThinking AI

When does prompt caching actually save money?

AI Architect

Key takeaways

  • Caching applies to a prompt prefix, so anything that varies must go after everything stable.
  • A cache write costs more than a normal input token; a read costs a fraction of one.
  • Break-even arrives after a small number of reads within the TTL, often two or three.
  • A single changed character early in the prompt invalidates the whole prefix, which is the most common cause of silent cache misses.

Prompt caching is the highest-leverage cost optimisation available to most LLM applications, and it is routinely implemented in a way that never hits. The mechanism is simple; the failure modes are all about prompt layout.

What is being cached, exactly?

A prefix, matched exactly from the first token.

Both Anthropic and OpenAI implement this the same way, with different pricing and expiry rules.

When you mark a cache breakpoint, the provider stores the processed representation of everything before it. A later request whose prompt begins with those identical tokens skips the reprocessing and reads the stored state instead.

Token cost relative to one base input token

Token cost relative to one base input tokenBar chart. Cache write: 1.25×. Base input token: 1×. Cache read: 0.1×.Cache write1.25×Charged once, when the prefix is first stored.Base input tokenCache read0.1×Charged on every subsequent hit within the TTL.
Show data
Token cost relative to one base input token. Bar chart. Cache write: 1.25×. Base input token: 1×. Cache read: 0.1×.
ItemValue (×)Note
Cache write1.25Charged once, when the prefix is first stored.
Base input token1
Cache read0.1Charged on every subsequent hit within the TTL.
Illustrative multipliers in the range providers commonly publish. Check your provider's current pricing before modelling on these.

Two consequences follow directly, and they explain almost every disappointing result:

  • The match is byte-exact and anchored at the start. Nothing about it is semantic or fuzzy.
  • One changed character early in the prompt invalidates everything after it.

One thing to be precise about. “Processed representation” means the provider stores the key/value attention state for those tokens, the KV cache, rather than the text. The work being skipped is the expensive part of inference, which is where the saving comes from. That state also occupies memory on serving hardware, so providers attach a short time-to-live, commonly a few minutes, refreshed on each hit.

Both facts point the same way. Caching rewards traffic that reuses a large prefix quickly, and does nothing for traffic that does not.

How do you lay out a cacheable prompt?

Order the prompt by how often each part changes, most stable first:

Prompt layout, ordered most stable to most volatile

Prompt layout, ordered most stable to most volatileDiagram: 7 ordered layers. System instructions and role definition, then Tool and function definitions, then Large reference documents, schemas, examples, then cache breakpoint (breakpoint), then Conversation history, then Current user input, then Timestamps, request IDs, retrieved passages.1System instructions and role definitionChanges on deploy, if ever2Tool and function definitionsChanges on deploy3Large reference documents, schemas, examplesThe bulk of the cached prefixcache breakpoint4Conversation historyGrows per turn; a 2nd breakpoint can follow5Current user input6Timestamps, request IDs, retrieved passagesMust never appear above the breakpoint
Show as text
Prompt layout, ordered most stable to most volatile. Diagram: 7 ordered layers. System instructions and role definition, then Tool and function definitions, then Large reference documents, schemas, examples, then cache breakpoint (breakpoint), then Conversation history, then Current user input, then Timestamps, request IDs, retrieved passages.
#LayerNote
1System instructions and role definitionChanges on deploy, if ever
2Tool and function definitionsChanges on deploy
3Large reference documents, schemas, examplesThe bulk of the cached prefix
·cache breakpoint (breakpoint)everything above is the cache key
4Conversation historyGrows per turn; a 2nd breakpoint can follow
5Current user input
6Timestamps, request IDs, retrieved passagesMust never appear above the breakpoint
A single varying character above the breakpoint invalidates the whole prefix, which is why per-request values belong at the bottom.
  1. System instructions and role definition
  2. Tool and function definitions (an MCP toolset lands here)
  3. Large reference documents, schemas, examples
  4. ← cache breakpoint
  5. Conversation history (or a second breakpoint that advances with the turns)
  6. The current user input
  7. Anything genuinely per-request: timestamps, IDs, retrieved passages

The classic mistake is injecting a current date or a request ID into the system prompt “for context”. It costs nothing to read and guarantees a total miss on every request.

The second most common is non-deterministic serialisation. If your tool schema is assembled from a dictionary and serialised without sorting keys, byte order can differ between processes or language versions, and the prefix silently stops matching on some machines but not others. It shows up as a hit rate stuck around 60% for no visible reason. Sort the keys, pin the ordering, and treat the cached prefix as a build artefact rather than something reassembled per request.

// Stable-to-volatile ordering. Everything before the breakpoint is the
// cache key, so a single varying character above it costs you every hit.
const messages = [
  { role: "system", content: [
      { type: "text", text: SYSTEM_INSTRUCTIONS },        // never changes
      { type: "text", text: JSON.stringify(TOOL_SCHEMA) },// changes on deploy
      { type: "text", text: REFERENCE_DOC,
        cache_control: { type: "ephemeral" } },           // <- breakpoint
  ]},
  ...history,                                             // grows per turn
  { role: "user", content: userInput },                   // varies every call
];
// WRONG, and the most common bug in the wild:
//   text: `Today is ${new Date().toISOString()}. ${SYSTEM_INSTRUCTIONS}`

When does prompt caching break even?

Let a base input token cost 1 unit. A cache write costs roughly 1.25 units, a cache read roughly 0.1. For a stable prefix of N tokens reused across R requests:

  • Without caching: N × R units
  • With caching: N × 1.25 for the first write, then N × 0.1 × (R − 1)

Cumulative cost of a 20,000-token prefix

  • No caching
  • With caching
Cumulative cost of a 20,000-token prefixLine chart of Cumulative cost against Requests within the cache TTL. No caching ranges from 1 to 50. With caching ranges from 1.25 to 6.15.012.52537.5501235102050Requests within the cache TTLCumulative cost (relative units)
Show data
Cumulative cost of a 20,000-token prefix. Line chart of Cumulative cost against Requests within the cache TTL. No caching ranges from 1 to 50. With caching ranges from 1.25 to 6.15.
Requests within the cache TTLNo cachingWith caching
111.25
221.35
331.45
551.65
10102.15
20203.15
50506.15
The lines cross between the first and second request. Past roughly two reads inside the TTL, caching is strictly cheaper and the gap widens without bound.

Setting them equal, caching wins once R is above roughly 1.3. The second request inside the TTL already pays for the write. Verify the exact multipliers against your provider’s current pricing, but the shape holds: the break-even is low enough that the real question is only whether your traffic reuses prefixes inside the expiry window.

The TTL is what actually decides this, far more than the multipliers. A five-minute window turns caching into a bet on request density: bursty traffic hits it, and evenly spread traffic quietly pays the write premium again and again. Some providers sell a longer window at a higher write price, worth buying precisely when your reuse is real but slow.

Workload Stable prefix Reuse in TTL Caching
Chat with long system prompt Large Every turn Strongly positive
Agent loop over a fixed toolset Large Every step Strongly positive
Document Q&A, many questions per doc Very large High Strongly positive
One-shot classification, unique inputs Small None Net loss
Batch job, distinct document each time None None Net loss

How do you confirm it is working?

Do not infer it from the bill. Both providers return per-request token counts that separate cache writes from cache reads, documented in the Anthropic usage fields and the equivalent OpenAI response object. Log those two numbers as a ratio and alert when the read share drops. A deploy that reorders tool definitions or adds a line to the system prompt will silently zero your hit rate, and the only visible symptom is a cost increase nobody attributes to the right change.

# Emit this per request. Alert on a sustained drop rather than one miss:
# a deploy that changes the prefix shows up here long before it shows up
# on the invoice.
def cache_health(usage) -> dict:
    read = usage.cache_read_input_tokens
    write = usage.cache_creation_input_tokens
    total = read + write + usage.input_tokens
    return {
        "hit_ratio": read / total if total else 0.0,
        "write_ratio": write / total if total else 0.0,
        "uncached": usage.input_tokens,
    }

Two things make that alert trustworthy. Compare against a rolling baseline rather than a fixed threshold, because a healthy hit rate differs per endpoint. A number that looks fine for a one-shot classifier is alarming for an agent loop. And bucket the metric by deploy version, so a regression points at the change that caused it rather than at the hour somebody noticed.

Treat misses as a correctness smell as well as a cost one. A prefix that stops matching almost always means something varied that you believed was constant, and that is worth understanding even when the money involved is trivial.

Do this

Turn on prompt caching and confirm it is actually hitting

Most caching work fails silently, so the ordering steps and the measurement steps matter equally.

  1. Sort your prompt blocks from never-changing to per-request

    System instructions first, then tool definitions, then large reference material. Conversation history, user input, timestamps and retrieved passages all go after.

  2. Serialise every block deterministically

    Sort object keys and pin array ordering before rendering. Non-deterministic JSON is the reason a hit rate sits near 60% with no obvious cause.

  3. Place the breakpoint at the last byte that never varies

    Everything above it becomes the cache key. Moving it even one block too low throws away most of the saving.

  4. Remove anything dynamic from above the breakpoint

    A current date in the system prompt is the classic case. It reads as harmless and guarantees a total miss on every request.

  5. Log cache reads, cache writes and uncached input per request

    Providers return all three. Track the read share as a ratio and bucket it by deploy version so a regression names the change that caused it.

  6. Alert on a sustained drop against a rolling baseline

    A fixed threshold will be wrong for at least one endpoint. Compare each route against its own recent history instead.

Frequently asked questions

What actually gets cached?
A contiguous prefix of the prompt, matched exactly from the beginning. It is not a semantic or fuzzy cache. The tokens must match byte for byte from the start of the prompt up to the cache breakpoint.
Why is my cache hit rate near zero?
Almost always because something variable appears early in the prompt: a timestamp, a request ID, a session identifier or a reordered tool list. Anything that changes per request has to sit after all cached content.
Does caching change the model's output?
No. It changes how the input is processed. What the model computes is unchanged. Identical inputs produce the same distribution over outputs whether or not the prefix was cached.
Is it worth it for a chatbot?
Usually yes, because the system prompt and conversation history form a growing stable prefix reused on every turn. It is single-shot classification and batch jobs with unique inputs where caching is a net loss.

Sources

  1. Prompt cachingAnthropic
  2. Prompt caching for faster model inferenceOpenAI

prompt-cachingcostlatencyproduction