How-To Guides

Semantic Caching for LLM Requests: Similarity Thresholds, Measured

Eren Şimşir· Founder & Chief Technical Editor•
A person is typing code on a laptop, focusing on the screen with programming script.
Photo by Lukas Blazek via Pexels

A 0.92 similarity threshold would almost never hit; a lower one serves wrong answers

I measured cosine similarity between real embeddings of 40 questions for a support-style semantic cache. At the commonly quoted threshold of 0.92, only 2 of 20 rephrased questions found their cached answer with gemini-embedding-2, and none with gemini-embedding-001. Lowering the threshold fixes that but starts serving wrong answers: pairs like "import vs export", "add vs remove" and "enable vs disable" scored higher than several genuine rephrasings. No single threshold separated the two groups.

  • Do not copy a threshold from an article. The two embedding models in my test needed very different values (see the table).
  • Calibrate on your own pairs: some real rephrasings and, more importantly, some real near-misses where one word changes the meaning.
  • The embedding call itself took a median of 569 ms from my machine, so a cache miss adds latency; with a 2.1 s model call, the cache pays off in latency above roughly a 27% hit rate.
  • A cached answer is only correct if the question really means the same thing. Cache stable FAQ-style answers, not anything that depends on state or on a changing word.

The test

On 9 October 2026 I embedded 50 texts with each of two models through the Gemini API (embedContent): 10 base questions that play the role of the cache, 20 rephrasings of them (two per question), 10 near-misses (one per question, with most words shared but a different intent) and 10 unrelated questions. Embeddings were compared with cosine similarity in NumPy; both models returned 3,072-dimensional vectors. For each query I took the most similar base question and called it a hit if its similarity reached the threshold, and I counted it as a correct hit only if it was the right base question. Examples:

Cached questionRephrasing (should hit)Near-miss (should not hit)
How do I add a teammate to my workspace?How can another person join my workspace?How do I remove a teammate from my workspace?
How do I export my data as CSV?Can I download my data in a CSV file?How do I import my data from CSV?
How do I enable two-factor authentication?Where do I turn on 2FA for my account?How do I disable two-factor authentication?

What the similarity scores looked like

Query groupgemini-embedding-2 (min / median / max)gemini-embedding-001 (min / median / max)
Rephrasings, similarity to their own question0.802 / 0.895 / 0.9430.710 / 0.781 / 0.912
Near-misses, best similarity to any cached question0.722 / 0.818 / 0.8870.636 / 0.706 / 0.872
Unrelated questions, best similarity0.504 / 0.553 / 0.5950.463 / 0.504 / 0.534

Unrelated questions are easy: they stay well below everything else. The hard part is the overlap between rephrasings and near-misses. With gemini-embedding-2 the weakest rephrasing scored 0.802 while the strongest near-miss scored 0.887, so no threshold between them gets all 20 hits and all 10 rejections. The three highest-scoring near-misses were "How do I import my data from CSV?" (0.887, matched to the export question), "How do I remove a teammate from my workspace?" (0.878, matched to the add question) and "How do I disable two-factor authentication?" (0.850, matched to the enable question).

What each threshold would have done

ThresholdRephrasings answered from cache (of 20), embedding-2Near-misses wrongly answered (of 10), embedding-2Rephrasings answered (of 20), embedding-001Near-misses wrongly answered (of 10), embedding-001
0.702010206
0.8020661
0.8516231
0.907010
0.922000
0.950000

Read the two models side by side: to avoid every wrong answer, gemini-embedding-2 needed 0.90, where it served only 7 of 20 rephrasings; gemini-embedding-001 needed 0.88, where it served 1 of 20. The same number means different things for different models. Unrelated questions were never served at any threshold from 0.70 up. No rephrasing was ever matched to the wrong base question.

The latency side

An embedding call is a network request. Over the 50 calls per model in this test it had a median of 569 ms (95th percentile 715 ms, slowest 760 ms) for gemini-embedding-2 and a median of 545 ms for gemini-embedding-001. For comparison I timed five short support-style answers from gemini-3.5-flash: 7.97 s for the first call and about 2.0 s for the other four (median 2.09 s). The arithmetic follows from those numbers: every request pays the 0.57 s embedding, a hit saves the 2.09 s generation, so the average latency improves only when the hit rate is above about 0.57 / 2.09, or 27%. The cache lookup itself, a matrix product over a few vectors, is negligible next to the embedding call. The figures depend on your network, region and models, so measure them yourself.

A minimal cache, with the threshold as a parameter

import numpy as np

class SemanticCache:
    """In-memory semantic cache: cosine similarity over unit vectors, one threshold, optional TTL."""
    def __init__(self, embed_fn, threshold, ttl_seconds=None):
        self.embed, self.threshold, self.ttl = embed_fn, threshold, ttl_seconds
        self.vectors, self.answers, self.stored_at = [], [], []

    def _unit(self, text):
        v = np.asarray(self.embed(text), dtype=float)
        return v / np.linalg.norm(v)

    def add(self, question, answer, now):
        self.vectors.append(self._unit(question)); self.answers.append(answer); self.stored_at.append(now)

    def lookup(self, question, now):
        if not self.vectors:
            return None, 0.0
        sims = np.vstack(self.vectors) @ self._unit(question)
        i = int(np.argmax(sims))
        expired = self.ttl is not None and now - self.stored_at[i] > self.ttl
        return (None if expired or sims[i] < self.threshold else self.answers[i]), float(sims[i])

I ran this class with threshold 0.80 on the stored embeddings. "How can another person join my workspace?" returned the cached answer (similarity 0.914), the unrelated capital-city question returned nothing (0.551), an expired entry returned nothing, and "How do I remove a teammate from my workspace?" also returned the answer cached for adding a teammate (0.878). That last line is the failure this article is about.

Similarity threshold: paraphrases served vs. wrong answers servedLine chart over thresholds 0.70 to 0.95, in percent. Paraphrases correctly served (of 20): gemini-embedding-2 falls from 100% to 0%, gemini-embedding-001 falls faster. Near-miss questions wrongly served (of 10) are dashed.Similarity threshold: paraphrases served vs. wronganswers servedparaphrases served, embedding-2paraphrases served, embedding-001near-misses wrongly served, embedding-2near-misses wrongly served, embedding-0010%25%50%75%100%.70.75.80.85.88.90.92.95cosine-similarity threshold
Percent of 20 paraphrases served correctly (solid) and of 10 near-miss questions wrongly served (dashed), from the two result files of the test above (3072-dimension Gemini embeddings). A higher threshold stops wrong answers but also stops almost every paraphrase from hitting.

Practical rules

  1. Calibrate with your own near-misses. Collect questions from your logs where one word flips the meaning (add and remove, enable and disable, import and export, free and paid) and make sure they fall below your threshold.
  2. Prefer misses to wrong hits. A miss costs one model call; a wrong hit gives the user a confident answer to a different question. Start with a high threshold and lower it only while the near-miss list stays clean.
  3. Only cache answers that stay valid. Add a time limit, and put the model name and system prompt in the cache key so a changed prompt does not serve old answers.
  4. Track both rates. Log the hit rate and the share of hits that a human or a check flags as wrong. I did not test a second-stage check (for example asking a model whether two questions mean the same), so I cannot say how well it works.

What I did not test

One English support-style set of 40 questions, two Gemini embedding models, no vector database (the 10 cached vectors sit in a NumPy array), no OpenAI or open-source embedding models, and no non-English text. Qdrant, Redis and other stores do the same similarity search; the thresholds above come from the embeddings, not from the store. A real FAQ will have more cached questions and more overlap, which makes wrong hits likelier.

Sources and Further Reading

Frequently Asked Questions

What similarity threshold should a semantic cache use?

There is no universal value. In my test with gemini-embedding-2, 0.92 served only 2 of 20 rephrased questions, and avoiding all wrong hits needed 0.90; with gemini-embedding-001 the equivalent value was about 0.88. Calibrate on your own questions with a list of near-misses, and recalibrate if you change the embedding model.

Why do opposite questions like add and remove score so high?

They share almost every word and the same topic, and embedding models place such sentences close together. In my test "How do I remove a teammate from my workspace?" scored 0.878 against the question about adding one, higher than some real rephrasings. A cache using a single threshold cannot tell them apart.

How much latency does an embedding lookup add?

In my runs one embedContent call took a median of 569 ms (95th percentile 715 ms) for gemini-embedding-2. Every request pays it, hit or miss, so with a 2.1 s model call the cache improves average latency only above roughly a 27% hit rate. Measure with your own network and models.

Do I need a vector database for a semantic cache?

Not to start. For a few thousand cached questions an in-memory NumPy matrix is enough; my 10-question cache used one. A vector store such as Qdrant or Redis helps when the cache is large, shared between processes, or must survive restarts.

Can a semantic cache return a stale or wrong answer?

Yes, in two ways: a near-miss question can match the wrong cached question, and a correct match can return an answer that is out of date. Use a threshold calibrated on near-misses, a time limit on entries, and a cache key that includes the model and system prompt.