Semantic Caching for LLM Applications: Cutting Cost and Latency
LLM API calls are expensive and slow. A GPT-4o call costs $0.005–0.015 per 1K tokens and returns results in 500–1500ms. For applications where many users ask semantically similar questions—customer support, FAQ chatbots, documentation assistants, internal knowledge bases—a large fraction of those calls are paying full price for answers already computed moments ago.
Exact-match caching solves a small part of this. If a user asks “what is your return policy?” and another user asks the identical string, a simple key-value cache serves the second request instantly. But “what’s your return policy?”, “can I return items?”, “how do returns work?”, “do you accept returns?” are all the same question from a cost perspective—the cached answer is equally correct for all of them. An exact-match cache misses every paraphrase.
Semantic caching addresses this by embedding each query into a high-dimensional vector and searching for similar previous queries. When a new query is close enough to a cached one—as measured by cosine similarity between their embeddings—the cached response is returned. The embedding lookup takes 15–25ms. The saved LLM call costs hundreds of milliseconds and real money.
At 67% cache hit rates, semantic caching reduces costs by 60–70% and improves median latency by 60–70%. These numbers vary dramatically with workload; a documentation assistant with repetitive questions hits 80%+ rates, while a creative writing tool hits under 5%. Knowing when to use it—and how to tune it—is the entire challenge.
How Semantic Caching Works
The pipeline has four steps: embed, search, decide, and return or compute.
Semantic cache pipeline:
Incoming query
│
▼
Embed query ──────────────────────────────────────────────────────┐
(embedding model, 15-25ms) │
│ │
▼ │
Vector similarity search │
(ANN search in cache store, 5-10ms) │
│ │
├─── similarity >= threshold ────────────────────────────────► Return cached response
│ (cache hit) (< 30ms total)
│
└─── similarity < threshold │
(cache miss) │
│ │
▼ │
Call LLM │
(500–1500ms) │
│ │
▼ │
Store (query_embedding, response) in cache │
│ │
▼ │
Return response
The Embedding Model
The embedding model converts text into a dense vector (typically 768–3072 dimensions). Queries with similar meaning cluster near each other in this space; dissimilar queries are far apart. The quality of the cache depends entirely on the quality of the embedding model—a model that encodes “how do returns work” and “what is your return policy” into nearby vectors will produce useful cache hits; a weak model will not.
Embedding model choice involves a latency/quality tradeoff:
Embedding model options for semantic cache:
Model | Dims | Latency | Quality | Cost | Notes
-------------------------|------|----------|----------|------------|------------------------
text-embedding-3-small | 1536 | ~50ms | good | $0.02/1M | OpenAI, simple to use
text-embedding-3-large | 3072 | ~70ms | v.good | $0.13/1M | Overkill for most caches
all-MiniLM-L6-v2 | 384 | ~5ms | decent | free | Local, small, fast
all-mpnet-base-v2 | 768 | ~15ms | good | free | Local, better quality
BAAI/bge-small-en-v1.5 | 384 | ~5ms | good | free | Best small local model
BAAI/bge-large-en-v1.5 | 1024 | ~20ms | v.good | free | Best large local model
nomic-embed-text-v1.5 | 768 | ~10ms | v.good | free | Local, strong on domains
For a production semantic cache, a local model like BAAI/bge-large-en-v1.5 is the right default: zero per-call cost, 20ms latency, and quality that matches or exceeds OpenAI’s small embedding model on most English-language tasks. Running it via sentence-transformers requires a one-time model download (~1.3 GB) and fits on a CPU.
Similarity Metrics
Cosine similarity is the standard metric for embedding spaces:
cosine_similarity(a, b) = (a · b) / (||a|| * ||b||)
Range: -1 to 1
1.0 = identical direction (semantically very similar)
0.0 = orthogonal (unrelated)
-1.0 = opposite direction (rare in practice for text)
In practice for text embeddings:
> 0.97 = near-identical phrasing
0.93-0.97 = same question, different words
0.88-0.93 = related topic, not identical question
0.80-0.88 = same general domain, different question
< 0.80 = different topics
L2 (Euclidean) distance also works and is equivalent to cosine similarity for unit-normalized vectors. Most vector databases store pre-normalized vectors and default to cosine or inner product distance.
Similarity Threshold: The Hard Part
The threshold is a single number that controls everything—cache hit rate, accuracy, and whether your users start getting wrong answers. There is no universal correct value. It must be tuned for your specific workload.
What Goes Wrong at Each Extreme
Threshold too low (e.g., 0.80): Queries that are topically related but semantically distinct get the same cached response. “What is your return policy?” and “what is your shipping policy?” have similar embeddings—both about policies—but different answers. At threshold 0.80, one might incorrectly serve the other’s cached response. Users get wrong answers. This is the worst failure mode.
Threshold too high (e.g., 0.99): Only near-verbatim rephrases hit the cache. “How do returns work?” and “how does the return process work?” might score 0.975—a miss. Cache hit rate drops to near zero, and you have added 25ms of lookup overhead for no benefit.
The workload dependence problem: Code queries cluster tightly in embedding space (technical vocabulary is precise; “sort a list in Python” and “Python list sorting” are extremely close). Conversational queries distribute more broadly. A single threshold performs poorly across mixed workloads—too aggressive for code, too conservative for conversation.
Tuning Procedure
|
|
A false positive rate of 1–3% (precision 0.97–0.99) is the practical target for most applications. Above this, you are occasionally giving users wrong answers; below it, you are leaving hit rate on the table. For customer-facing support, aim for 0.99+. For internal tooling where users can recognize wrong answers and re-ask, 0.95 is acceptable.
Cache Invalidation
The hardest problem in semantic caching is invalidation. When the correct answer to a question changes—prices update, policies change, documentation is revised—cached responses become stale. Unlike exact-match caches where you can invalidate by key, semantic caches cannot be selectively invalidated by content without re-embedding everything.
TTL (Time-to-Live)
The simplest approach: every cache entry expires after a fixed time. Set TTL based on how frequently the underlying data changes.
TTL guidelines by content type:
Content type | Suggested TTL
--------------------------------|------------------------
Static docs (rarely change) | 24–72 hours
Product information | 4–12 hours
Pricing / availability | 30–60 minutes
Real-time data (weather, stock) | Do not cache
Code documentation | 48–168 hours
FAQ / support content | 6–24 hours
Add jitter (random variance) to TTL values to prevent thundering herd—without it, entries created at the same time expire simultaneously and cause a burst of LLM calls.
|
|
Version-Based Invalidation
When you update a knowledge base, increment a version number and store it with each cache entry. On lookup, only return hits that match the current version.
|
|
Event-Driven Invalidation
For content that changes on specific events (product catalog update, policy change, new release), trigger cache flushes directly from your CI/deployment pipeline.
|
|
Failure Modes
Semantic Collision on Dense Query Types
Some query domains cluster very tightly in embedding space. Legal questions, financial questions, and medical questions are all phrased carefully and embed near each other—but their answers diverge dramatically. “What are the tax implications of a Roth IRA?” and “What are the tax implications of a traditional IRA?” embed at 0.93 similarity, but their answers are importantly different.
Mitigation: Use category-aware thresholds. Detect query category (via a classifier or keyword matching) and apply a stricter threshold for high-stakes domains.
|
|
Context Dependence
Semantic similarity is computed on the query text, not on the full context. Two identical questions asked by different users, or in different conversation states, may warrant different answers. “What is my account balance?” is the same question semantically but requires user-specific responses—it should never hit a shared cache.
Mitigation: Namespace cache by user/session for personalized queries. Only use a shared cache for queries whose correct answer is independent of user identity.
|
|
Embedding Model Drift
If you change embedding models, all existing cache entries have incompatible embeddings. A cached response from all-MiniLM-L6-v2 cannot be meaningfully compared to a new query embedded with BAAI/bge-large-en-v1.5.
Mitigation: Store the embedding model name and version with each cache entry. On model upgrade, flush all entries (or filter to current model version). For zero-downtime upgrades, run both models in parallel during a transition period.
Implementation: From Scratch with Redis and FastAPI
Redis with the redis-search module (RediSearch) provides vector similarity search alongside traditional key-value operations. This makes it a natural cache store—you get TTL, persistence, and ANN search in one system.
|
|
|
|
FastAPI Proxy Wrapper
|
|
|
|
LiteLLM Semantic Cache
LiteLLM’s proxy server has built-in semantic caching that works transparently across all supported providers. It is the fastest path to production for teams already using LiteLLM.
|
|
|
|
|
|
Your application calls http://localhost:8000/v1/chat/completions with any supported provider, and LiteLLM handles caching transparently. The X-Cache response header indicates hit/miss.
|
|
GPTCache
GPTCache (Zilliz) is a standalone semantic caching library designed for direct embedding into Python applications. It integrates with LangChain and llama_index and supports a wider range of backends than LiteLLM.
|
|
|
|
GPTCache’s similarity_threshold is a distance threshold rather than a similarity threshold—0.0 means identical, 1.0 means completely different. A threshold of 0.8 roughly corresponds to cosine similarity ≥ 0.8. The naming is inverted from most other systems; be careful when porting thresholds.
Monitoring Cache Performance
Instrument your cache to measure the metrics that actually matter: hit rate, false positive rate, and latency distributions.
|
|
Key dashboards to build:
- Hit rate by namespace (should be stable; sudden drops indicate workload shift or cache flush)
- P50/P95 lookup latency (embedding dominates; should be < 30ms at P95)
- False positive rate (track via user thumbs-down or explicit correction signals)
- Cache entry age distribution (shows staleness risk—entries older than TTL should be zero)
When Semantic Caching is Worth It
Use case fitness for semantic caching:
Use case | Cache fit | Expected hit rate | Notes
--------------------------------|------------|-------------------|---------------------------
FAQ / support chatbot | excellent | 60–85% | High query repetition
Documentation assistant | excellent | 70–90% | Stable content, similar qs
Internal knowledge base | excellent | 65–85% | Closed domain, repetition
Product search / catalog | good | 40–65% | Some variation in phrasing
Code generation (same patterns) | good | 30–60% | Tight embedding clustering
Customer support (personalized) | partial | 20–40% | Namespace by user required
Creative writing | poor | < 10% | Each request unique
Real-time data queries | poor | near 0% | Answers change constantly
Multi-turn conversation | poor | < 10% | Context-dependent
Agent tool calls | poor | < 5% | Action-oriented, varied
The investment in semantic caching pays off proportionally to query repetition and answer stability. If your workload is customer support, documentation search, or any other closed-domain Q&A where the same questions arrive frequently and the answers change on day/week timescales, semantic caching is one of the highest-ROI infrastructure investments available.
If your workload is generative (creative writing, unique coding tasks, multi-turn personalized dialogue), cache infrastructure adds complexity and latency overhead for minimal benefit. Do the hit rate analysis on your actual query logs before building the cache.
The break-even point is roughly a 15–20% hit rate: at that level, the saved LLM calls offset the embedding overhead and the operational cost of maintaining the cache. At 60%+ hit rates, semantic caching halves your LLM spend with no visible impact on response quality.
Comments