1
Normalise and scope the key
before any lookup
Why: the key decides both the hit rate and the safety of every response cache.
- Normalise: lowercase, trim whitespace, strip punctuation, so "How do I return shoes?" and "how do i return shoes" match.
- Scope: tenant, user permissions or role, locale, model version, prompt version.
- Key = hash(scope + normalised input).
"The cache key includes tenant, permissions, locale, model version and prompt version. I never serve user A's personalised answer to user B."
keying on the question text alone. That is how one customer sees another customer's order status.
2
Exact response cache
saves the whole call
Why: the cheapest possible hit. Same normalised input in the same scope returns the stored output in about a millisecond.
- Works well for FAQs, buttons and suggested prompts, and repeated workflow steps.
- Hit rate on free text is low, because people phrase things differently.
- Usually a key-value store such as Redis, with a TTL.
expecting a high hit rate on open chat. Measure it before you promise savings.
3
Semantic cache
saves the whole call
Why: catches paraphrases. Embed the query, find the nearest stored query in the same scope, and reuse its answer if the similarity is above a threshold.
- The threshold is the whole game. Too low gives wrong answers, too high gives no hits.
- "Cancel my order" and "cancel my subscription" can sit very close in embedding space and need different answers.
- Tune the threshold on a labelled set of near-miss pairs, not by eye.
- "Adapting" a cached answer with a small LLM call is possible, but it gives back some of the saving.
"Similar is not the same as equivalent, so I tune the threshold against near-miss pairs and only use the semantic cache for low-risk questions."
one global threshold for every intent. Questions with entities in them, like order IDs and dates, should skip it.
4
Embedding, retrieval and tool caches
saves sub-calls
Why: on a miss, the request still embeds text, searches an index and calls APIs. Each of those can be cached on its own.
- Embedding cache: key = hash(text + embedding model version). Invalidate everything when the embedding model changes, because old and new vectors are not comparable.
- Retrieval cache: query plus filters gives the top-k chunk IDs. Invalidate when the index is rebuilt or the documents change.
- Tool results: "return policy for shoes" can live for hours. "Order status" lives for seconds or not at all.
one TTL for everything. Set the TTL from how fast that data really changes.
5
Prompt prefix caching
saves input cost and time to first token
Why: the model reuses the computed attention state (the KV cache) for a prompt prefix it has already processed, so it skips that work. Providers do this server-side, and vLLM does it with automatic prefix caching.
- Only an identical prefix hits. One changed token early in the prompt invalidates everything after it.
- Put static content first (system prompt, tool definitions, examples, shared documents) and the variable parts last (user message, timestamps, user name).
- Provider caches are short-lived, usually minutes, and often need a minimum prefix length. Check your provider's docs.
- This one doesn't return stored answers, so it can't serve a wrong answer. It only makes the call cheaper and faster.
"Most of my input tokens are the same system prompt and tools on every call, so I put them first and let prefix caching discount them."
a timestamp or the user's name at the top of the system prompt. It makes every prefix unique and the hit rate drops to zero.
6
Store, expire and invalidate
after the call
Why: a cache without invalidation rules is a bug you haven't noticed yet.
- Store the response with its scope key and a TTL that fits the content.
- Don't store refusals, errors or answers that failed a guardrail check.
- Invalidate on events: a policy document is edited, a model or prompt version ships, the index is rebuilt.
- Monitor hit rate per layer and sample cached answers for quality, just like fresh ones.