AI agents
Key points

Key points

  1. 1Prefix caching reuses the model's key-value (KV) states only when the prompt start is byte-for-byte identical, so any reordering or reformatting of retrieved chunks breaks the hit.
  2. 2Sorting retrieved chunks by a stable key (source ID, then score) makes the chunk block reproducible across queries, which turns an unpredictable prefix into a shareable one.
  3. 3Cache misses are not free mistakes: providers bill cache reads at a small fraction of the base input rate and writes at a premium, so unstable prefixes cost real money.
  4. 4Measure cached-token share per prompt template with logging, then re-run the measurement after every prompt or index change, because a normal deploy can halve the hit rate.

Why chunk order decides your cache hit rate

Retrieval augmented generation has a quiet failure mode. Two queries arrive with the same instructions and three of the same four retrieved chunks, yet the provider computes the whole prefix again because the fourth chunk, or the order of the four, differs. The model had already done that work; the prompt just did not repeat it exactly.

Prefix caching matches from the first token forward. The serving engine finds the longest cached token sequence that matches the new request from the beginning, loads those KV states, and runs prefill only for the remaining tokens. Whitespace and formatting count: a single character difference breaks the match (Modular's inference handbook). That is the mechanical reason chunk order matters. Retrieval ranks chunks by similarity score, and scores shift with every query and every embedding refresh. If you concatenate chunks in rank order, the prefix changes constantly and the cache can only ever match the system prompt.

Retrieval pipelines produce a hard case for prefix caching. A given chunk is repeatedly retrieved across user questions, but it can appear at arbitrary locations with arbitrary neighboring context, so naive reuse of its cached KV states degrades output quality (Cache-Craft, arXiv). You cannot reorder chunks freely for cache reasons without regard to what the model needs to read, so the ordering policy has to serve both goals at once.

The payoff is worth the discipline. Providers discount cached input heavily: Anthropic's pricing page lists cache hits and refreshes at a fraction of the base input rate and cache writes at a premium over it (Claude platform docs). One engineering team reported cache hit rate rising from 7% to 84% after restructuring how it marked and ordered stable prefixes, with overall cost falling 59% on the same token volume (ProjectDiscovery).

The three layers people confuse

Practitioners use "cache" for four different mechanisms, and mixing them up leads to the wrong fix. The following table separates them.

LayerWhat is storedMatch ruleWhat breaks it
KV cacheAttention tensors for one requestExact token prefixNothing you control; it is per request
Prefix cachingThose tensors, kept on the serverHash chain over token IDsAny change to tokens, whitespace or serialization
Prompt cachingThe provider's billed version of that lookupSame prefix match, plus TTL and routingPrefix churn, TTL expiry, request landing on a different replica
Response cachingFinished response stringsHash of the whole requestAny parameter change; by design it returns nothing on a miss

The first three are exact-match and correctness-neutral: a miss costs money and latency, nothing else. Semantic caching is fuzzy by design and can return a wrong answer with a 200 status (Daily Dose of Data Science). For a retrieval pipeline, the lever you actually control is the prefix: the system prompt, tool definitions, and the chunk block. Of those, the chunk block is the part that churns.

Response caching is a different product. It hashes the request body, model, API key and streaming mode, then returns the stored response without calling the provider, with cached responses arriving in 80 to 300 milliseconds and zero billed tokens (OpenRouter announcement). It is excellent for test suites and agent retries. It does nothing for two similar questions that retrieve overlapping chunks, because the hashes differ. Prompt caching is the layer that helps there, and it only helps if the chunks land in the same place.

A worked example: from rank order to a stable prefix

Take a support assistant. The static system prompt is 1,200 tokens and tool definitions add 800. Retrieval pulls four chunks of about 500 tokens each, so the variable block is 2,000 tokens and the user question is 30. Total prompt: about 4,030 tokens.

Before the fix, chunks are concatenated in descending similarity score. Query A retrieves chunks 12, 45, 78, 91 at scores 0.91, 0.88, 0.84, 0.80. Query B, twenty seconds later, retrieves 45, 12, 103, 78 at scores 0.89, 0.87, 0.85, 0.81. The shared prefix between the two requests stops after the 2,000 static tokens. Every request pays full input price for roughly 2,000 chunk tokens and gets a cache read on 2,000 static tokens at best.

Now the fix. Sort the retrieved chunks by a stable key before serialization, for example source_id ascending, with score as a tiebreaker. Query A becomes 12, 45, 78, 91. Query B becomes 12, 45, 78, 103. The two serialize identically up to the end of chunk 78, which is 3,500 tokens into the prompt. The model reuses everything up to chunk 78 and prefills only chunk 103 plus the question: about 530 tokens instead of 2,030.

Run the numbers on a representative batch of 10,000 requests with 60% sharing that three-chunk prefix:

  • Static tokens per request: 2,000. Chunk tokens per request: 2,000.
  • Without stable ordering, assume 20% of requests hit the static prefix only. Cached tokens: 0.2 × 2,000 × 10,000 = 4.0M. Full-price tokens: 24.0M.
  • With stable ordering, assume 60% of requests hit through the third chunk. Cached tokens: 0.6 × 3,500 × 10,000 = 21.0M. Full-price tokens: 19.3M.

At a base input rate of $2 per million tokens with cache reads at one twentieth of that (the ratio shown for several models on Anthropic's pricing table), the second configuration costs $0.10 × 21.0 plus $2 × 19.3 = $40.70, against $0.10 × 4.0 plus $2 × 24.0 = $48.40 for the first. That is roughly 16% off the input bill on a modest sharing assumption. Push the shared-prefix share higher and the gap widens. The exact discount depends on your provider and model; substitute your own read and write rates before quoting any figure.

The ordering code is small. Here is the pattern in Python:

def canonical_order(chunks):
    # Stable key first, relevance second. Ties broken by chunk id.
    return sorted(chunks, key=lambda c: (c["source_id"], -c["score"], c["chunk_id"]))

def render_context(chunks):
    # No timestamps, no request ids, no dict key order variance.
    lines = []
    for c in canonical_order(chunks):
        lines.append(f"[source:{c['source_id']}#{c['chunk_id']}]\n{c['text'].strip()}")
    return "\n\n".join(lines)

Two details matter more than the sort itself. First, strip and normalize whitespace in chunk text, because a trailing space reintroduced by a different PDF extractor breaks the chain. Second, do not interpolate a request ID, timestamp or user name anywhere above the chunk block. Those belong after the variable content, or in a header the provider does not tokenize into the prefix. OpenAI's guidance is the same shape: put static content at the beginning and variable content at the end (OpenAI prompt caching).

Where the prefix gets destroyed downstream

Even a perfect ordering function can be undone by infrastructure. Prefix caching depends on the serving engine keeping the entry and routing the request to a worker that holds it (Modular). In a distributed deployment, a standard load balancer scatters related requests across pods, destroying cache locality and forcing recomputation. One benchmark of prefix-aware scheduling reports 57 times faster response times and double the throughput on identical hardware compared with naive routing (llm-d). If you self-host, the scheduler is part of your cache strategy.

If you call a provider through a gateway, check whether it does sticky routing. OpenRouter keeps subsequent requests on the same provider endpoint after a cached request, with sessions expiring after ten minutes of inactivity, and it hashes the first system message and first non-system message to identify conversations by default (OpenRouter prompt caching docs). That means a stable first system message does double duty: it anchors the cache key server-side and the routing key gateway-side. You can also pass an explicit session identifier for multi-turn agent flows where opening messages change.

Caching also has a time dimension. Anthropic's cache has a five-minute default lifetime, refreshed free on each use, and the lifetime is measured from the start of the request that writes or reads the entry, so a slow streamed response eats into the window (Claude platform docs). A retrieval corpus that changes hourly is a poor fit for long-lived prefixes unless you accept that the chunk block will miss and only the static head hits. Amazon Bedrock users get the same tradeoff between implicit and explicit caching, with support varying by model and API (Bedrock prompt caching). Decide deliberately which layer you are optimizing.

A checklist for retrieval pipelines

The following sequence is the order that produces measurable wins fastest.

  1. Freeze the head. System prompt, tool definitions, output schema and few-shot examples go first, in a fixed order, with no interpolated variables. Tools should be sorted: static tools alphabetically, per-user dynamic tools last, which is the pattern one production team used to keep a shared cache chain identical across users (ProjectDiscovery).
  2. Canonicalize chunk order. Sort by source ID, then score, then chunk ID. Write the comparator into the retrieval module, not into each caller.
  3. Version the serialization. Add a single version string to the head and bump it when the format changes on purpose, so you can correlate a hit-rate drop with a deploy. Do not add it per request.
  4. Deduplicate and cap. Overlapping chunks from the same document waste prefix space and shift positions. Dedupe by document and text hash before ordering, then take the top N by score and re-sort canonically. Chunk size and boundaries already dominate retrieval quality, so fix chunking before tuning cache behavior (Weaviate on chunking).
  5. Pin the route. Use session stickiness or prefix-aware routing so requests with the same head reach the same worker.
  6. Measure. Log cached tokens and total prompt tokens per request, then compute the share as cached prompt tokens divided by total prompt tokens. Track response-cache hits separately, because the two mechanisms have different economics and should not be blended into one number (Metabase).
  7. Alert on regressions. A prompt template change that breaks prefix stability can silently halve the hit rate and surface later as a cost spike (Metabase). Alert per app, not on the fleet average, which moves too slowly to catch anything.

A quick decision rule for step 4: if your top-N retrieval overlaps by fewer than one chunk across a typical query pair, chunk-ordering work will not pay for itself and you should focus on the static head instead. If overlap is three or more chunks, canonical ordering is the highest-return change available.

Measuring hit rate honestly

Most teams measure the wrong thing. The useful metric is a token-weighted share, not a request count, because a request that hits 100 tokens out of 4,000 is not a success. The SQL pattern below computes the share and an estimated saving per app over a rolling window, adapted from the metric definition at Metabase:

SELECT app_name,
       SUM(cached_tokens)::numeric / NULLIF(SUM(prompt_tokens), 0) AS cached_share,
       SUM(prompt_tokens - cached_tokens) AS full_price_tokens
FROM llm_request_rollups
WHERE window_start >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY app_name
ORDER BY full_price_tokens DESC;

Two caveats. First, cached reads are discounted, not free, and writes can cost extra, so compute savings from recorded costs rather than hits multiplied by list price. Second, expect a ceiling. A workload with a large stable system prompt should see a high cached-token share; a long-tail creative feature with little repeated context may honestly sit near zero. Judge each workload against its own ceiling.

For teams running retrieval across several model providers, the ordering logic should live above the provider boundary, so a model swap does not silently reset your cache behavior. Quanta Inference exposes text, image, video, audio and embedding models through one OpenAI-compatible API key, which keeps the request shape constant while you compare hit rates across models: https://quantaintelligence.ai/inference

FAQ

FAQ

Does sorting chunks by score hurt answer quality?

It can, slightly, because models read the beginning and end of a long context more reliably than the middle. The fix is to sort canonically and then reorder only a small tail, or to place the highest-scoring chunk first while keeping the rest in stable order. Measure answer quality on a fixed eval set before and after, since the ordering change is cheap to revert.

What is a good prefix cache hit rate?

It depends on your workload's repetition. Support and search style features with a large stable system prompt should see a high cached-token share, while long-tail creative work may sit near zero. The practical target is a share that stays flat or rises after each deploy, and any unexpected drop is the signal to investigate.

Does chunk reordering change what the model answers?

For factual retrieval, usually not, because attention over the same set of chunks is largely order-insensitive at the margins. It can matter for multi-hop questions where one chunk depends on another. Run your eval set, and if quality moves, keep the canonical order but pin a small number of dependency-linked chunks to fixed positions.

Why does my hit rate drop after a deploy?

Prompt template changes are the usual cause. Provider caches key on a stable prefix, so reordering the system prompt, bumping a chunk format, or injecting a timestamp near the top invalidates every entry at once. Version the serialization deliberately and correlate deploys with hit-rate charts.

Is this worth doing before fixing chunking and retrieval quality?

No. Chunk size, boundaries and retrieval accuracy determine whether the answer is any good, and caching only makes a good pipeline cheaper. Fix retrieval quality first, then use canonical ordering to convert that quality into cost and latency savings.

Sources

Sources

  1. Prefix caching - LLM Inference Handbook - Modular
  2. Cache-Craft: Managing Chunk-Caches for Efficient ...
  3. Prompt caching - Claude Platform Docs
  4. How We Cut LLM Costs by 59% With Prompt Caching
  5. KV vs Prefix vs Prompt vs Semantic Caching
  6. Response Caching: Zero Cost for Identical Requests - OpenRouter
  7. Prompt caching | OpenAI API
  8. KV-Cache Wins You Can See: From Prefix Caching in vLLM to ... - llm-d
  9. Prompt Caching - Optimize AI Model Costs with Smart Caching
  10. Prompt caching for faster model inference - Amazon Bedrock
  11. Chunking Strategies to Improve LLM RAG Pipeline ...
  12. Cache Hit Rate (LLM): Definition, SQL & How to Track It in ...
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles