MODULE 12  ยท  Protocols & Standards

Semantic Caching: Cache by Meaning, Not Exact Match

Exact-match caching is boring. Same question, same answer. Semantic caching matches similar questions โ€” and that changes everything.

๐Ÿ“… Mar 2026
โฑ 10 min read
๐ŸŽฏ Episode 83 of 98
Semantic CachingOptimizationEmbeddingsCost
In this episode

You've seen exact-match caching. Same question, same answer. Fast, cheap, boring.

But users don't ask the same question twice. They ask:

"What's the capital of France?"

"France capital city?"

"Which city is France's capital?"

Different strings. Same meaning. Same answer.

Semantic caching recognizes this. It caches by meaning, not by characters. And it can cut your AI costs by 40-70%.

The Problem with Exact-Match Caching

Traditional cache:

Key: "What's the capital of France?"

Value: "Paris"

Next query: "What is the capital of France?"

Result: MISS โŒ (different string)

Even tiny variations break the cache:

Extra spaces

Different capitalization

Synonyms

Word order changes

In practice, exact-match caching hits 10-20% of the time for natural language queries. Semantic caching hits 40-70%.

โšก

How Semantic Caching Works

The Flow

User Query โ†’ Embed โ†’ Search Cache (vector similarity) โ†’

If similar > threshold: Return cached response

Else: Call LLM โ†’ Store (embedding, response) in cache

Step-by-Step

  1. Embed the query

python

from sentence_transformers import SentenceTransformer

snippet
code
model = SentenceTransformer('all-MiniLM-L6-v2')

def get_embedding(text: str):

example
code
return model.encode(text)
snippet
code
query = "What's the capital of France?"
query_embedding = get_embedding(query)  # 384-dimensional vector
  1. Search the cache

python

import numpy as np

from sklearn.metrics.pairwise import cosine_similarity

snippet
code
cache = [
{"embedding": [...], "response": "Paris", "query": "France capital?"},
    {"embedding": [...], "response": "Berlin", "query": "Germany capital?"},

]

snippet
code
def find_in_cache(query_emb, threshold=0.92):
for entry in cache:
        similarity = cosine_similarity([query_emb], [entry["embedding"]])[0][0]
        if similarity > threshold:
            return entry["response"], similarity
    return None, 0
  1. Return or fetch

python

def get_response(user_query: str):

example
code
query_emb = get_embedding(user_query)
    cached_response, similarity = find_in_cache(query_emb)
example
code
if cached_response:
        return {"response": cached_response, "source": "cache", "similarity": similarity}
example
code
# Cache miss - call the LLM
    llm_response = call_llm(user_query)
example
code
# Store in cache
    cache.append({
        "embedding": query_emb,
        "response": llm_response,
        "query": user_query
    })
example
code
return {"response": llm_response, "source": "llm"}

๐Ÿ”ง

Choosing the Right Threshold

The similarity threshold is critical. Too high: misses good matches. Too low: returns wrong answers.

Threshold Behavior Best For

0.98+Nearly exact matches onlyCode, structured data
0.92-0.95Same meaning, different wordsGeneral Q&A
0.85-0.90Related topicsResearch, exploration
<0.85Loose associationNot recommended

Domain-Specific Tuning

python

THRESHOLDS = {

example
code
"faq": 0.93,        # High precision needed
    "code_help": 0.95,  # Code is precise
    "brainstorming": 0.88,  # More tolerance for variation
    "support": 0.92     # Balance precision and recall

}

๐Ÿ“Š

Production Implementation with GPTCache

Don't build this from scratch. Use GPTCache:

python

from gptcache import Cache

from gptcache.adapter import openai

from gptcache.embedding import Onnx

from gptcache.similarity_evaluation.distance import SearchDistanceEvaluation

Initialize cache

snippet
code
cache = Cache()

cache.init(

example
code
embedding_func=Onnx(),  # Fast, local embedding model
    data_manager=data_manager,
    similarity_evaluation=SearchDistanceEvaluation(),

)

Patch OpenAI to use cache

openai.cache = cache

Now all your OpenAI calls are semantically cached

snippet
code
response = openai.chat.completions.create(
model="gpt-3.5-turbo",
    messages=[{"role": "user", "content": "What's the capital of France?"}]

)

Vector Storage Options

Store Best For Scale

In-memory (dict)Prototyping, single server<10K entries
SQLite + vector extensionSmall apps, easy setup<100K entries
RedisFast, distributed cache<1M entries
PostgreSQL + pgvectorExisting Postgres infraUnlimited
PineconeManaged, high scaleMillions+
QdrantSelf-hosted, fastMillions+

Redis Example

python

import redis

import json

import numpy as np

snippet
code
r = redis.Redis(host='localhost', port=6379)

def cache_response(query_emb, response):

example
code
# Store as hash: embedding_id -> {embedding, response}
    emb_id = f"emb:{hash(query_emb.tobytes())}"
    r.hset(emb_id, mapping={
        "embedding": query_emb.tobytes(),
        "response": json.dumps(response)
    })
snippet
code
def find_similar(query_emb, threshold=0.92):
# Scan all entries (use vector DB for production scale)
    for key in r.scan_iter(match="emb:*"):
        entry = r.hgetall(key)
        cached_emb = np.frombuffer(entry[b"embedding"])
        similarity = cosine_similarity([query_emb], [cached_emb])[0][0]
        if similarity > threshold:
            return json.loads(entry[b"response"]), similarity
    return None, 0
๐Ÿ’ก

Advanced Techniques

  1. Response Validation

Before returning a cached response, verify it's still relevant:

python

snippet
code
def validate_cache_hit(query: str, cached_response: str) -> bool:
"""Use a cheap model to verify cache hit validity"""
    validation_prompt = f"""
    User asked: {query}
    Cached answer: {cached_response}
Is this cached answer appropriate for the user's question?
    Reply only YES or NO.
    """
    result = call_cheap_llm(validation_prompt)
    return "YES" in result.upper()
  1. Cache Warming

Pre-populate cache with common queries:

python

snippet
code
common_queries = [
"How do I reset my password?",
    "What are your business hours?",
    "How do I contact support?"

]

for query in common_queries:

example
code
response = call_llm(query)
    embedding = get_embedding(query)
    store_in_cache(embedding, response)
  1. Time-Based Expiration

Some responses become stale:

python

snippet
code
def store_with_ttl(query_emb, response, ttl_hours=24):
cache_entry = {
        "embedding": query_emb,
        "response": response,
        "timestamp": time.time(),
        "ttl": ttl_hours * 3600
    }
    # Store in Redis with expiration
    r.setex(f"emb:{id}", ttl_hours * 3600, json.dumps(cache_entry))

๐Ÿ”ฌ

Measuring Cache Performance

Track these metrics:

python

class SemanticCacheMetrics:

example
code
def __init__(self):
        self.hits = 0
        self.misses = 0
        self.similarities = []
example
code
@property
    def hit_rate(self):
        total = self.hits + self.misses
        return self.hits / total if total > 0 else 0
example
code
@property
    def estimated_savings(self):
        # Assuming $0.002 per API call
        return self.hits * 0.002

Expected Results by Use Case

Use Case Hit Rate Monthly Savings

Customer support FAQ60-75%โ‚น2-5 lakhs
Code documentation40-55%โ‚น1-3 lakhs
General Q&A bot50-65%โ‚น1.5-4 lakhs
Translation service45-60%โ‚น1-2.5 lakhs

๐Ÿ›ก๏ธ

When NOT to Use Semantic Caching

โŒ Time-sensitive queries โ€” "What's the weather today?"

โŒ Personalized responses โ€” "What's my account balance?"

โŒ Creative writing โ€” Users want fresh content

โŒ Exact data retrieval โ€” SQL queries, specific IDs

โœ…

Factual Q&A, โœ… Common support questions, โœ… Code explanations, โœ… Definition lookups

๐Ÿ“ฆ

Practical Takeaways

Semantic caching hits 3-4x more than exact-match โ€” Same meaning, different words

Threshold tuning is critical โ€” 0.92-0.95 works for most use cases

Use GPTCache โ€” Don't build from scratch

Validate cache hits โ€” Quick LLM check prevents stale responses

Warm the cache โ€” Pre-populate with your top 100 queries

Set TTLs โ€” Some answers expire (pricing, features, policies)

Measure savings โ€” Track hit rates and cost reduction

๐Ÿš€

What's Next?

Episode 84: AI Gateway Architecture โ€” OpenRouter, LiteLLM, unified APIs, fallbacks, and provider abstraction. The infrastructure layer that makes semantic caching even more powerful.

โ† Previous

Ep 82: Webhooks & Event-Driven AI

Next โ†’

Ep 84: AI Gateway Architecture

Next: Episode 84 โ€” AI Gateway Architecture

OpenAI, Claude, Gemini, self-hosted Llama โ€” managing all of them is a nightmare. An AI gateway unifies them all.

This is part of a 98-episode series covering AI engineering from tokens to production deployment.

โ† Previous Ep 82: Webhooks & Event-Driven AI: Async Architectures That Scโ€ฆ