Blog/LLMOps & evals

Semantic caching for LLM apps: when reusing answers is safe, and when it is not

How semantic caches reuse LLM answers for similar questions, how they differ from prompt caching, and how to avoid wrong hits, leaks and stale answers.

··6 min read

  • Semantic cache
  • LLM cost
  • Latency
  • Embeddings
  • Redis
A new question is embedded and compared with cached questions; above a similarity threshold the stored answer is returned, below it the model is called.

Key takeaways

  • Prompt caching reuses an identical prefix inside the provider and never changes the answer; a semantic cache returns a stored answer for a similar question.
  • In a published customer-service style test, a semantic cache cut API calls by up to 68.8% with positive hit rates above 97%.
  • The similarity threshold is a product decision: set it from labelled pairs of questions that must and must not share an answer.
  • Scope cache keys by tenant, user, language and data version, or one customer can receive another customer's answer.
  • Cache only answers that do not depend on fresh data or personal context, and give every entry a TTL and an invalidation path.

Listen to this article

0:000:00

Support assistants and FAQ bots answer the same questions over and over: how do I reset my password, where is my invoice, what are your delivery times. Every one of those questions costs a model call. A semantic cache stores the answer the first time and returns it when a later question means the same thing, even if it is worded differently. Done well, it removes a large share of calls and answers in milliseconds. Done carelessly, it returns confident answers to questions nobody asked.

This article separates semantic caching from provider prompt caching, shows the architecture, and spends most of its time on the parts that decide whether it is safe: the threshold, cache scope, what not to cache, and invalidation.

How is semantic caching different from prompt caching?

AspectPrompt caching (provider)Semantic caching (yours)
MatchExactly identical prefixSimilar meaning above a threshold
What is reusedProcessed input tokensThe whole stored answer
Model still runsYes, the answer is freshly generatedNo, on a hit
Risk of a wrong answerNone from cachingYes, on a false hit

Claude's prompt caching docs are explicit that hits require 100% identical prompt segments and that caching has no effect on output generation. That makes it safe by construction, and you should turn it on first; I cover it in prompt caching and model routing. A semantic cache sits in front of the model and is a different kind of decision, because it changes what the user receives.

How does a semantic cache work?

Semantic cache lookupThe question is normalized and embedded, a vector search finds the nearest cached question within the same scope, and the stored answer is returned if the similarity passes the threshold; otherwise the model is called and the new answer is stored with a TTL.QuestionnormalizeEmbedsame modelSearchwithin scopeHit?≥ thresholdAnswercached or new
On a miss the model answers and the result is written back, so the cache fills from real traffic.

Libraries such as GPTCache split this into an embedding generator, cache storage for the answers, a vector store for the questions and a similarity evaluator that decides whether a candidate is a match. You do not need a library for it: an embeddings table with a vector index in Postgres, such as pgvector, and twenty lines of code are enough.

# Illustrative semantic cache on Postgres + pgvector
def cached_answer(question: str, scope: dict) -> str:
    q = normalize(question)
    vec = embed(q)                                       # same model for writes and reads
    row = db.fetchone("""
        SELECT answer, 1 - (embedding <=> %(v)s) AS sim
        FROM answer_cache
        WHERE tenant = %(tenant)s AND lang = %(lang)s AND data_version = %(ver)s
          AND expires_at > now()
        ORDER BY embedding <=> %(v)s LIMIT 1""", {"v": vec, **scope})
    if row and row.sim >= THRESHOLD:
        metrics.hit(row.sim)
        return row.answer
    answer = call_model(question, scope)
    if cacheable(question, answer):
        db.execute("INSERT INTO answer_cache (...) VALUES (...)")   # with expires_at = now() + ttl
    return answer

How much does a semantic cache save?

The GPT Semantic Cache paper stored query embeddings in Redis and tested repetitive, customer-service style queries. It reports up to 68.8% fewer API calls, cache hit rates between 61.6% and 68.8% depending on the category, and positive hit rates, hits that returned a correct answer, above 97%. Those numbers come from traffic chosen to be repetitive. A product FAQ looks like that; an open-ended assistant or a coding tool does not, and will see a small fraction of the hits.

Before building anything, measure it: log a week of questions, embed them, and count how many have a near neighbor that a human would accept as the same question. That number is your ceiling.

How do you choose the similarity threshold?

Embedding similarity measures topic, not intent. How do I cancel my order? and How do I cancel my subscription? are very close in vector space and need different answers. Can I return opened items? and Can I return unopened items? differ by one prefix and have opposite answers. A threshold tuned on average similarity will happily serve one for the other.

  1. Collect a few hundred pairs of real questions and label each pair: same answer or different answer. Include the tricky negatives deliberately.
  2. Compute the similarity of each pair with the embedding model you will use in production.
  3. Pick the lowest threshold at which wrong hits stay at or near zero, then accept whatever hit rate that gives you.
  4. Re-run the check whenever you change the embedding model, because similarity scales differ between models.

A second check helps at the margin: for hits just above the threshold, ask a small, cheap model whether the two questions have the same answer. That costs far less than generating the answer and removes most of the near misses.

How do you keep a semantic cache from leaking data?

A cache is shared memory between users. If the answer to what is my current discount? is cached without the customer id in the key, the next customer who asks gets the first one's discount. Scope every lookup by everything the answer depends on:

  • Tenant and, for personal answers, user. Or exclude personal answers from the cache entirely.
  • Language, so a German question does not receive a cached English answer.
  • Role or permission level, if internal staff see more than customers.
  • Data and prompt version: a new price list, policy text or system prompt should start a fresh cache namespace.

Cached answers containing personal data are personal data at rest, with the retention duties that implies; see PII redaction in LLM pipelines. A cache is also a write path: if users can get arbitrary model output stored and served to others, a prompt-injected answer can spread. Only store answers from trusted flows, and never from sessions flagged by your guardrails.

How do you expire and invalidate cached answers?

Use three mechanisms together. A TTL on every entry, measured in hours or days rather than months. A version in the cache key for the data and prompt the answer came from, so a content update switches to a fresh namespace instead of deleting rows one by one. And a manual purge by document: when someone edits the returns policy, delete every entry whose answer cited that document. That last one requires storing the source ids with each cached answer, which is worth doing anyway for citations, as in grounding and citations.

What should you monitor?

MetricWhy
Hit rate per scopeShows where the cache pays off
Similarity distribution of hitsMany hits near the threshold mean risk
Thumbs-down rate on cached vs fresh answersThe direct signal of wrong hits
Sampled human review of hitsCatches wrong hits users did not report

Trace cache hits like any other step, with the similarity score and the id of the cached entry, so a wrong answer can be traced back to the question it was originally generated for; see observability for LLM agents.

Sources

  1. Regmi and Pun: GPT Semantic Cache: Reducing LLM Costs and Latency via Semantic Embedding Caching (2024)
  2. Zilliz: GPTCache README (modules, similarity evaluation, eviction)
  3. Claude Platform Docs: Prompt caching
  4. pgvector README: vector types and indexes

Frequently asked questions

What is the difference between prompt caching and semantic caching?

Prompt caching is a provider feature that reuses the processed prefix of a prompt when it is exactly identical; the model still generates a new answer. Semantic caching is your own layer that returns a previously generated answer when a new question is similar enough to a cached one, skipping the model call entirely.

How much can a semantic cache save?

It depends on how repetitive the traffic is. The GPT Semantic Cache paper reports up to 68.8% fewer API calls on repetitive customer-service style queries, with cache hit rates of 61.6% to 68.8% and positive hit rates above 97%. Open-ended chat sees far fewer hits.

What similarity threshold should I use?

There is no universal number; it depends on the embedding model and the domain. Collect pairs of real questions labelled as same answer or different answer, and pick the threshold that keeps wrong hits near zero on that set.

Is a semantic cache safe with personal data?

Only if cache entries are scoped to the user or tenant they belong to, and answers that contain personal data are either excluded or cached per user. A shared cache that ignores scope can leak one user's answer to another.

When should I not use a semantic cache?

When answers depend on live data such as stock, prices or order status, on the conversation so far, or on the user's permissions. In those cases cache the expensive parts, such as retrieval results or tool outputs, not the final answer.

Sounds like what you need?

Tell me about your project or role – I’d love to hear from you.