Semantic Caching
Answering a question you have answered before when it is not phrased the same way — the threshold that decides it, the cost of getting it wrong, and what must never be cached.
Three different caches
| Exact | Prefix (KV) | Semantic | |
|---|---|---|---|
| Key | hash of the full request | the literal token prefix | embedding of the query |
| Stored | the response | attention keys and values | the response |
| Saves | the entire generation | prefill for the shared prefix | the entire generation |
| Lives | anywhere (Redis) | on the GPU that holds it | a vector index |
| Can be wrong | no | no | yes |
| Typical hit rate | low (1–10%) | high with shared system prompts | workload-dependent |
Exact and prefix caching are transparent: a hit returns exactly what a miss would have. Semantic caching is not — it returns the answer to a different question that was judged similar enough. That single difference is why it needs a cost model and the other two do not.
Key idea
Prefix caching and semantic caching are frequently discussed as one thing and they are not comparable. Prefix caching is an optimisation with no correctness surface, and by the superlinear saving in Caching — Hit Rates, Zipf and the Working Set it should be on by default. Semantic caching is a product decision with an error rate. Ship the first without discussion; ship the second only with measurement.
Does it pay?
Let be the fraction of requests answered from the cache, the fraction of those that are wrong answers, the cost of generating, the cost of a lookup, and the cost of serving a wrong answer. Then
and the cache is worth deploying exactly when
Key idea
The governing ratio is : the price of a generation against the price of being wrong. For a support bot where a wrong answer costs a human escalation worth perhaps a hundred generations, the tolerable false-hit rate is under 1%. For an internal search suggestion, where a wrong answer costs a click, it might be 20%. The same cache with the same threshold is a good idea in one product and a bad idea in the other, and no amount of tuning changes which.
The threshold
Let the cache return the nearest stored entry when its cosine similarity exceeds . Both the hit rate and the false-hit rate are non-increasing in . Hence no change of can reduce false hits and increase hits simultaneously: the only way to improve both is to change the embedding, not the threshold.
A reference implementation
class SemanticCache:
def __init__(self, index, embedder, threshold=0.93, ttl=3600):
self.index, self.embed, self.tau, self.ttl = index, embedder, threshold, ttl
def _key(self, req) -> str:
# Everything that changes the answer must be in the namespace, not the vector.
return hashlib.sha256(json.dumps({
"model": req.model,
"system": req.system, # a different system prompt is a different cache
"tools": sorted(t.name for t in req.tools),
"tenant": req.tenant, # never share across tenants
"config": req.config_hash,
}, sort_keys=True).encode()).hexdigest()
async def lookup(self, req):
if not cacheable(req):
return None
v = await self.embed(normalise(req.query))
hit = await self.index.nearest(namespace=self._key(req), vector=v, k=1)
if hit is None or hit.score < self.tau:
return None
if hit.age > self.ttl:
await self.index.delete(hit.id)
return None
metrics.incr("semcache.hit", score=hit.score)
return hit.payload
async def store(self, req, response):
if not cacheable(req) or response.stop_reason != "end_turn":
return # never cache a truncated or errored generation
v = await self.embed(normalise(req.query))
await self.index.upsert(namespace=self._key(req), vector=v, payload=response)
What must never be cached
Heuristic — a reasoning shortcut that predicts the right answer in the regimes discussed here. It is not a proof and can fail outside them.
A request is unsafe to serve from a similarity match when any of the following hold:
- Personalised — the answer depends on who is asking ("what is my balance?"). The query text may be identical across users and the answers are not.
- Time-sensitive — the answer depends on now ("is the service up?", "what did we ship this week?"). A TTL bounds the damage but does not remove it.
- Side-effecting — the request causes a tool call that does something. Serving a cached "I've cancelled your order" without cancelling anything is a serious failure mode.
- Authorisation-dependent — the answer depends on the caller's permissions. Caching across principals leaks data upward.
- Non-deterministic by design — brainstorming, creative writing, sampling with high temperature, where returning the same answer twice defeats the purpose.
The practical form is a cacheable(req) predicate consulted before both lookup and store, with the
default being not cacheable and an explicit allowlist of intents that are.
Key idea
Semantic caching works best in a narrow band: high-volume, low-variance, impersonal, read-only questions — FAQ-shaped traffic. That band is often a large fraction of a support workload and almost none of an agent workload. Measuring which you have, before building the cache, is the step most often skipped.
Measuring it honestly
A semantic cache needs two numbers that ordinary caches do not:
- Hit rate — easy, a counter.
- False-hit rate — hard, because a false hit looks exactly like a hit. Obtaining it requires deliberately sampling a small fraction of hits, generating the true answer anyway, and comparing. That shadow generation costs a few percent of the savings and is the only way to know the rate rather than assume it.
if hit is not None and random.random() < SHADOW_RATE: # e.g. 0.02
asyncio.create_task(shadow_compare(req, hit)) # off the response path
What this does not say
Nothing here addresses invalidation when the underlying truth changes — the refund policy is rewritten and every cached answer about refunds is now wrong. TTL bounds the exposure but does not target it. Targeted invalidation requires tracking which source documents contributed to which cached answer, which is a lineage problem rather than a caching one and is taken up in Data Pipelines and Lineage.
References
- PaperF. Bang (2023). GPTCache: An Open-Source Semantic Cache for LLM ApplicationsDescribes the embed-and-search cache architecture used as the reference design here.
- PaperW. Gill, M. Elidrisi, P. Kalapatapu, A. Anwar, M. A. Gulzar (2024). MeanCache: User-Centric Semantic Cache for Large Language Model Based Web ServicesReports hit-rate and false-hit measurements against similarity thresholds.
These works are cited as sources to read, not reproduced. All exposition, examples and problems on this site are written independently; where an argument is standard, the citation points to a canonical treatment.
Results stated on this page
- DefinitionThree tiers of caching, doing three different jobsExact caching, prefix caching and semantic caching differ in what they key on, what they save and how they can be wrong.
- PropositionThe expected cost of a semantic cacheproof includedA semantic cache pays off only when the hit rate times the saving exceeds the false-hit rate times the cost of a wrong answer.
- PropositionLowering the threshold trades precision for recall monotonicallyproof includedHit rate and false-hit rate are both non-increasing in the similarity threshold, so there is no threshold that improves both.
- HeuristicWhat must never be cachedAnything personalised, time-sensitive, side-effecting or authorisation-dependent is unsafe to serve from a similarity match.