Caching › Code Implementation

Michael BrenndoerferFebruary 11, 202610 min read
Language AI HandbookAll chapters
Part 65 of 67Production Systems
Chapter 496Caching › Code Implementation
Reading section 7 of 11 · Code Implementation · 10 minSection 7 of 11 · 10 min

Book contents

Section 7 of 11 · 10 min read

Caching › Code ImplementationOne section · Sections in this chapter7 / 11 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Let's implement a semantic caching layer from scratch. We will build the core logic: embedding-based similarity search, cache storage, threshold-controlled retrieval, and hit rate tracking.

In[4]:
Code
# We will use sentence-transformers for embeddings
# uv pip install sentence-transformers
from sentence_transformers import SentenceTransformer

# Load a small, fast embedding model suitable for caching
embedding_model = SentenceTransformer("all-MiniLM-L6-v2")

The all-MiniLM-L6-v2 model produces 384-dimensional embeddings and runs in about 5 ms per query on a CPU. This is fast enough to add minimal latency to the cache lookup path.

In[5]:
Code
@dataclass
class CacheEntry:
    query: str
    response: str
    embedding: np.ndarray
    created_at: float
    hit_count: int = 0


@dataclass
class SemanticCache:
    threshold: float = 0.90
    ttl_seconds: float = 3600.0
    entries: list = field(default_factory=list)
    hits: int = 0
    misses: int = 0

    def _cosine_similarity(self, a: np.ndarray, b: np.ndarray) -> float:
        """Compute cosine similarity between two unit-normalized vectors."""
        return float(
            np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-10)
        )

    def _is_expired(self, entry: CacheEntry) -> bool:
        return (time.time() - entry.created_at) > self.ttl_seconds

    def lookup(self, query: str) -> Optional[str]:
        """Search cache for a semantically similar query. Returns response or None."""
        query_embedding = embedding_model.encode(
            query, normalize_embeddings=True
        )
        now = time.time()

        best_similarity = 0.0
        best_entry = None

        for entry in self.entries:
            if (now - entry.created_at) > self.ttl_seconds:
                continue
            sim = self._cosine_similarity(query_embedding, entry.embedding)
            if sim > best_similarity:
                best_similarity = sim
                best_entry = entry

        if best_entry is not None and best_similarity >= self.threshold:
            best_entry.hit_count += 1
            self.hits += 1
            return best_entry.response
        else:
            self.misses += 1
            return None

    def store(self, query: str, response: str) -> None:
        """Store a new query-response pair in the cache."""
        embedding = embedding_model.encode(query, normalize_embeddings=True)
        entry = CacheEntry(
            query=query,
            response=response,
            embedding=embedding,
            created_at=time.time(),
        )
        self.entries.append(entry)

    @property
    def hit_rate(self) -> float:
        total = self.hits + self.misses
        return self.hits / total if total > 0 else 0.0

The SemanticCache class encapsulates the full lookup-store-metric cycle. The lookup method encodes the incoming query, scans all unexpired entries for the best cosine similarity match, and returns a cached response only if the similarity exceeds the threshold. The store method encodes a query and adds it to the entry list. The hit_rate property computes the running hit fraction across all requests seen so far.

In[6]:
Code
# Simulated LLM responses for demonstration
def mock_llm(query: str) -> str:
    """Simulate an LLM call with a small delay."""
    time.sleep(0.05)  # Simulate 50ms inference time
    responses = {
        "What is the capital of France?": "The capital of France is Paris.",
        "What is the capital of Germany?": "The capital of Germany is Berlin.",
        "Explain neural networks briefly.": "Neural networks are computational models inspired by the brain, consisting of layers of interconnected nodes that learn patterns from data.",
        "What are transformers in NLP?": "Transformers are a neural network architecture that uses self-attention to process sequences in parallel, forming the basis of models like BERT and GPT.",
    }
    for key, val in responses.items():
        if any(word in query.lower() for word in key.lower().split()):
            return val
    return f"Response to: {query}"


# Populate the cache with some initial queries
cache = SemanticCache(threshold=0.88, ttl_seconds=3600)

initial_queries = [
    (
        "What is the capital of France?",
        mock_llm("What is the capital of France?"),
    ),
    (
        "Explain neural networks briefly.",
        mock_llm("Explain neural networks briefly."),
    ),
    (
        "What are transformers in NLP?",
        mock_llm("What are transformers in NLP?"),
    ),
]

for query, response in initial_queries:
    cache.store(query, response)
In[7]:
Code
# Test with queries at varying semantic distances from cached content
test_queries = [
    "What is the capital of France?",  # Exact match
    "Which city is the capital of France?",  # Paraphrase (should hit)
    "Where is France's capital located?",  # Rephrased (should hit)
    "Tell me about French neural networks.",  # Mixed topics (may hit or miss)
    "What is the capital of Japan?",  # Different country (should miss)
    "How do transformers work in deep learning?",  # Paraphrase (should hit)
]

results = []
for query in test_queries:
    start = time.time()
    cached_response = cache.lookup(query)
    latency_ms = (time.time() - start) * 1000

    if cached_response:
        source = "CACHE HIT"
        response = cached_response
    else:
        source = "CACHE MISS"
        response = mock_llm(query)
        cache.store(query, response)

    results.append(
        {
            "query": query,
            "source": source,
            "latency_ms": latency_ms,
        }
    )
Out[8]:
Console
Query                                              Result          Latency
---------------------------------------------------------------------------
What is the capital of France?                     CACHE HIT         7.9ms
Which city is the capital of France?               CACHE HIT        19.9ms
Where is France's capital located?                 CACHE HIT         7.0ms
Tell me about French neural networks.              CACHE MISS        6.9ms
What is the capital of Japan?                      CACHE MISS        7.8ms
How do transformers work in deep learning?         CACHE MISS        7.8ms

Overall hit rate: 50.0%
Total hits: 3  |  Total misses: 3
Out[9]:
Console

Cache entry usage:
Original Query                                            Hits
-----------------------------------------------------------------
What is the capital of France?                               3
Explain neural networks briefly.                             0
What are transformers in NLP?                                0
Tell me about French neural networks.                        0
What is the capital of Japan?                                0
How do transformers work in deep learning?                   0

The output shows the core behavior: exact and near-paraphrase queries return cached responses with millisecond latency, while queries about different topics miss the cache and trigger the (simulated) inference call. The hit count per entry reveals which cache entries are doing the most work, a useful metric for understanding whether your seed queries were representative of actual traffic.

Now let's examine how the hit rate changes as a function of the similarity threshold:

In[10]:
Code
# Simulate how threshold affects hit rate vs. response quality

# A set of base queries and their known paraphrases
query_groups = [
    {
        "canonical": "What is the capital of France?",
        "variants": [
            ("What is the capital of France?", 1.00),  # Exact
            ("Which city is France's capital?", 0.93),  # Near paraphrase
            ("France capital city name?", 0.87),  # Compressed form
            ("Where is France's government located?", 0.75),  # Related concept
            ("Name the French capital city.", 0.91),  # Rephrased
        ],
    },
    {
        "canonical": "Explain transformer architecture.",
        "variants": [
            ("Explain transformer architecture.", 1.00),
            ("How does the transformer model work?", 0.92),
            ("What is a transformer neural network?", 0.88),
            ("Describe the attention mechanism.", 0.72),
            ("What are key components of transformers?", 0.85),
        ],
    },
]

thresholds = np.round(np.arange(0.60, 1.01, 0.02), 2)
hit_rates = []
correct_match_rates = []

for threshold in thresholds:
    hits = 0
    correct = 0
    total = 0
    for group in query_groups:
        for variant_query, true_similarity in group["variants"]:
            total += 1
            if true_similarity >= threshold:
                hits += 1
                # A "correct" match is one where the similarity is genuinely high
                if true_similarity >= 0.85:
                    correct += 1
    hit_rates.append(hits / total)
    correct_match_rates.append(correct / total)
Out[11]:
Visualization
Line chart showing hit rate and correct-match rate versus similarity threshold, with a reference line at 0.88 and the curves first diverging at the plotted 0.74 threshold.
Cache hit rate and correct-match rate across similarity thresholds in the ten-query toy sample. Lowering the threshold from 1.0 admits more variants. At the marked 0.88 threshold, every admitted variant meets the sample's 0.85 correctness criterion. The plotted curves first diverge at 0.74, which admits a related query with 0.75 similarity as a false cache hit.

The plot shows the fundamental tradeoff. In this small, discrete sample, a threshold of 0.88 admits 60% of the variants and every admitted variant satisfies the chosen correctness criterion. Lowering the threshold to 0.84 admits all eight high-similarity variants without adding a false match. At the next relevant plotted setting, 0.74, the hit-rate curve moves above the correct-match curve because the 0.75-similarity query is now admitted. A production threshold still requires a much larger labeled validation set, but the point where these curves separate is the quantity to watch.

Now let's visualize the relationship between cache size, hit rate, and the cumulative latency savings for a realistic workload:

In[12]:
Code
# Simulate a request workload with a Zipfian query distribution
# (a small number of popular queries, a long tail of rare ones)

n_unique_queries = 500
n_total_requests = 5000
inference_latency_ms = 800
cache_lookup_ms = 15

# Zipf distribution: query i has probability proportional to 1/i
ranks = np.arange(1, n_unique_queries + 1)
zipf_probs = 1.0 / ranks
zipf_probs /= zipf_probs.sum()

# Sample request sequence
query_ids = np.random.choice(
    n_unique_queries, size=n_total_requests, p=zipf_probs
)

# Simulate caching with various cache sizes (max unique entries before eviction)
cache_sizes = [10, 25, 50, 100, 200, 500]
simulated_hit_rates = []
simulated_avg_latencies = []

for max_size in cache_sizes:
    cache_store = {}  # Maps query_id -> True (just track presence)
    cache_order = []  # LRU eviction order
    hits = 0

    for qid in query_ids:
        if qid in cache_store:
            hits += 1
            # Move to end (most recently used)
            cache_order.remove(qid)
            cache_order.append(qid)
        else:
            # Cache miss: add to cache
            if len(cache_order) >= max_size:
                # Evict least recently used
                evicted = cache_order.pop(0)
                del cache_store[evicted]
            cache_store[qid] = True
            cache_order.append(qid)

    hit_rate_sim = hits / n_total_requests
    avg_latency = hit_rate_sim * cache_lookup_ms + (1 - hit_rate_sim) * (
        inference_latency_ms + cache_lookup_ms
    )

    simulated_hit_rates.append(hit_rate_sim)
    simulated_avg_latencies.append(avg_latency)
Out[13]:
Visualization
Line chart of cache hit rate vs max cache size, showing rapid early rise then plateau
Cache hit rate as a function of maximum cache size for a simulated Zipfian query workload. Hit rate rises sharply through the first 50-100 entries, then continues with diminishing gains as the cache covers progressively rarer queries.
Line chart of average latency vs max cache size, showing rapid early fall then plateau
Average request latency as a function of cache size for the same workload. The dashed 800 ms line is the uncached inference baseline. Latency drops fastest in the early cache-size range and then approaches the 15 ms lookup cost more gradually, mirroring the diminishing hit-rate gains.
Out[14]:
Console
Cache Size | Hit Rate | Avg Latency | Latency Reduction
------------------------------------------------------------
        10 |   24.9% |        615ms |             23.1%
        25 |   40.6% |        490ms |             38.7%
        50 |   52.4% |        395ms |             50.6%
       100 |   67.1% |        278ms |             65.2%
       200 |   79.5% |        179ms |             77.6%
       500 |   90.8% |         88ms |             88.9%

The pattern in the output reflects a property of real language workloads: a Zipfian distribution means a small cache covers a disproportionately large fraction of traffic. The first 50 entries in the cache often account for 40-50% of all requests. This is why semantic caches deliver value even when they store only hundreds of entries against a universe of millions of possible queries. The diminishing returns visible in the hit rate curve confirm that there is a natural stopping point for cache investment: beyond a certain size, additional entries cover only very rare queries that contribute little to overall performance.