Vibe Engines
YouTube
AI System Design

Design an LLM Cache

Learn AI system design by building a prompt & response caching layer for LLMs step by step.

The numbers to beatO(1)hash lookupnormalizethen hash the whole requesttemp 0safest to cache

The whole design, in writing

Learn AI system design by building a prompt & response caching layer for LLMs step by step. An interactive guide to a two-tier cache — exact-match hashing, semantic (embedding) lookup with a tuned similarity threshold, TTL and invalidation, server-side prompt-prefix (KV) reuse, and hit-rate economics — so repeated and near-duplicate prompts skip the model and cut cost and latency.

Every step of the build above, written out: the problem each piece solves, the option that was taken and the ones that were not, the numbers, and how it fails in production.

The big idea

Why cache LLM calls at all?

Every LLM call costs real money and hundreds of milliseconds to seconds. And a huge fraction of production traffic is repetitive: the same FAQ, the same system-prompt-plus-document, a thousand slightly-reworded versions of the same question. Paying full price and full latency for an answer you’ve already computed is pure waste. How do you stop re-generating what you already know?

Put a caching layer in front of the model: check for a stored answer to this (or a near-identical) prompt before calling the LLM. A hit costs almost nothing and returns in milliseconds. It’s the single highest-leverage optimization for read-heavy LLM workloads — the cheapest call is the one you never make.

Step 1 · The skeleton

Cache-aside around the model

A prompt arrives. You want to return a cached answer if you have one, and otherwise call the model — and remember the result. What’s the control flow?

ApplicationCache APILLM Provider
New in this step: Application, Cache API, LLM Provider. · swipe to pan the diagram

What’s the right caching pattern in front of an LLM?

  1. Storing without checking first means you never actually serve from cache — you pay full price on every request. The lookup has to come before the model call.

  2. Classic cache-aside (read-through): look up first, serve on hit, and on a miss call the model and populate the cache so the next identical request is free.

  3. The prompt space is effectively infinite — you can’t enumerate it. You cache what actually gets asked, lazily, as it’s asked.

The Cache API runs cache-aside: normalize the request, look it up, return on a hit, and on a miss call the LLM Provider and write the result into the store before returning. Apps call the same completion contract — they never know whether an answer was cached or freshly generated.

Why this piece earns its place

Cache-aside has two failure modes that only show up under load. The first is the miss stampede: a hot prompt expires, and every request that arrives before the first one finishes also misses, so you pay the model once per caller for a single answer — at exactly the moment traffic is highest. The defense is single-flight: the first miss takes a lock on the key and the rest wait on its result. The second is what you write back. A provider timeout, a rate-limit error, and a response cut off at the token limit are all results, and storing one pins a broken answer for the entire TTL. Only write back completions that finished normally. There is also one knob that belongs in the request rather than the config: a per-call bypass, so a caller who knows their prompt is fresh can skip the lookup and repopulate the entry. Defaults live in config; exceptions live per request — and you will want that flag the first time a bad answer gets cached.

What the new pieces do

Applicationclient
A service that sends prompts to an LLM. Many are repeats or near-duplicates — the caching layer’s reason to exist. It never learns whether a hit was cached or freshly generated.
Cache APIbackend
The read-through front door: normalize the request, check the exact then semantic cache, and on a miss call the model and store the result — all behind the same completion contract.
LLM Providermodel
The real (expensive, slow) model call. Reached only on a cache miss; its result is written back into the cache for next time.

Step 2 · The trivial win

Exact-match cache on a normalized key

The safest hit is an identical request. But "identical" is subtle — whitespace, key order, and default params vary. What do you hash, and when is a hit safe to return verbatim?

Cache APIcache-asideExact Cachehash lookupResponse Storecached answers
New in this step: Exact Cache, Response Store.

What makes a good exact-cache key for an LLM call?

  1. The same prompt at temperature 0.9 vs a different model gives a different answer — the key must include the parameters and model, or a hit returns the wrong thing.

  2. Random ids never collide, so nothing ever hits. The key must be a deterministic function of the request’s meaningful content.

  3. Normalize (trim, canonical JSON, sorted keys), then hash prompt + model + all sampling params. Identical inputs → same key → a byte-identical, safe-to-reuse answer.

Key the Exact Cache on a hash of the normalized request: canonicalized prompt/messages plus model, temperature, max-tokens, and tool definitions. A hit is byte-identical and always safe. This tier is cheap, O(1), and catches the surprisingly large share of traffic that repeats exactly — especially deterministic (temperature 0) calls.

Why this piece earns its place

The key is a schema, and schemas change. The day you add tool definitions to the hash — or fix the normalizer so JSON keys sort — every existing entry becomes unreachable. Nothing errors and nothing is deleted; the orphaned entries sit there earning nothing until their TTL runs out, while hit-rate drops off a cliff. So carry a version prefix in the key, expect the cliff, and size for two key generations coexisting through the changeover. The normalizer itself deserves a caution, because its two directions fail asymmetrically. Normalize too little and you lose hits, which shows up on a chart. Normalize too much — lowercasing the prompt, collapsing whitespace inside a code block — and two genuinely different requests land on one key, which shows up as a wrong answer and nowhere else. And notice what this tier structurally cannot reach. In a chat product the key covers the whole message array, so every turn after the first carries a new suffix and the exact cache rarely fires again. The repetition is still there — the system prompt and few-shot block that every request shares, and a conversation replaying its own earlier turns inside its later ones — it just is not answer-shaped, which is the gap step 4 exists to fill.

  • O(1)hash lookup
  • normalizethen hash the whole request
  • temp 0safest to cache

What the new pieces do

Exact Cachecache
A key→value store keyed by a hash of the normalized (prompt + model + params). O(1) lookup; a hit is byte-identical and trivially safe to reuse.
Response Storestore
The stored completions with their metadata: source prompt, model+version, tenant, created-at and TTL. Both cache tiers resolve to an entry here.

Step 3 · Catch the near-duplicates

Semantic cache with a similarity threshold

Exact matching misses "How do I cancel?" vs "How can I cancel my plan?" — same intent, different bytes, no hit. Most repetition is semantic, not literal. How do you reuse an answer for a prompt that means the same thing?

Embedderprompt → vectorSemantic IndexANN + threshold
New in this step: Embedder, Semantic Index.

How do you get a cache hit on a reworded-but-equivalent prompt?

  1. Text normalization catches trivial variants but not genuine rewordings or synonyms — "cancel" vs "end my subscription" never align at the string level. You need meaning, not spelling.

  2. The Embedder maps the prompt to a vector; the Semantic Index finds the nearest cached prompt; if similarity clears a tuned threshold, its stored answer is reused. Meaning-level matching.

  3. That’s an extra model call per request — you’d spend the money you’re trying to save. Embedding + ANN does the equivalence check in milliseconds.

Add a semantic tier: the Embedder vectorizes the prompt, the Semantic Index (ANN) finds the nearest cached prompt, and if similarity clears a tuned threshold its stored answer is reused. Now near-duplicates hit too. That threshold is the whole game: too tight and you miss real duplicates; too loose and you serve wrong answers — the failure this page’s chaos button triggers.

Why this piece earns its place

The read path gets all the attention; the write path is what you end up operating. Every miss inserts another prompt vector, so the index grows with your traffic rather than with any corpus you control, and it fills with near-duplicates that sat just under the threshold — each one a separate entry with its own answer to keep fresh. Removing one is not the cheap delete the hash tier gets. A delete here is a soft delete: the point stops being returned immediately, so correctness is fine, but the graph still carries it, and you keep paying for its memory and its search work until a periodic rebuild reclaims the space. Defer that rebuild long enough and the graph degrades around the dead nodes and recall drifts — a slow, unalarming loss of hits. Budget it. The other thing to build now is the score log: record the top similarity on every lookup, including the ones that miss. Hits alone have no denominator — you see the wrong answers you served and never the right ones you declined. With the misses recorded you can re-score a week of real traffic against a candidate threshold offline and watch exactly which decisions would flip, before any of them reaches a user.

  • embed + ANNfind near-duplicates
  • thresholdhit-rate vs correctness
  • same embedderas the cached prompts

What the new pieces do

Embeddermodel
Embeds the incoming prompt so the semantic cache can find a near-duplicate. Must be the same model+version that embedded the cached prompts — or the distances are meaningless.
Semantic Indexindex
An ANN index over cached-prompt embeddings. Returns the nearest cached prompt; a hit counts only if similarity clears a tuned threshold — the knob between hit-rate and correctness.

Back of the envelope

tune on real traffic
pick the threshold from measured false-hit rate
scope per tenant
prompts carry private context — never cross tenants
re-verify borderline
a cheap check on marginal hits catches mismatches
same model+version
query and cached prompts must share embedding space

Step 4 · Reuse the prefix too

Prompt-prefix (KV) caching on a miss

Even on a cache miss, most requests share a huge, identical prefix — a long system prompt, a few-shot block, a retrieved document — followed by a short unique question. The model re-processes that whole prefix every time. Can a miss be cheaper?

ApplicationCache APIExact CacheEmbedderSemantic IndexResponse StoreLLM Provider
The system as it stands at this step. · swipe to pan the diagram

Two requests share a 4k-token system prompt but differ in the last line. How do you save?

  1. Server-side prompt-prefix caching stores the transformer’s key/value tensors for the shared prefix; a new request with the same prefix skips prefill on those tokens and only computes the new suffix — a big latency and cost cut even on a response-cache miss.

  2. Trimming the prompt can hurt quality and still recomputes what remains. Prefix caching keeps the full prompt but stops paying to reprocess it.

  3. A response-cache miss can still reuse computation. Prefix/KV caching makes the shared part of the prompt nearly free, which is most of the tokens.

Layer prompt-prefix caching: the provider (or your inference server) caches the computed KV state for a shared prefix, so a request reusing that prefix skips prefill on those tokens and only processes the new suffix. It cuts cost and time-to-first-token even when the response cache misses — because the expensive part of a long prompt is the prefix everyone shares.

Why this piece earns its place

How much of this you own depends on whose GPUs it runs on. On your own inference server the KV cache is yours: you set the memory budget, you choose the eviction policy, and the prefix-cache hit rate is a number on the server’s metrics endpoint. With a managed provider it is mostly automatic — some let you mark a prefix explicitly and buy it a defined lifetime, but where the caching is implicit you get no TTL and no direct hit-rate, only the cached-token count reported back in the response usage. Read that count either way, because the way this breaks is silent and easy to cause. Prefix matching is byte-exact from the front, so anything variable at the top of the prompt drops the hit-rate to zero: a timestamp in the system prompt, a request id, the user’s name, a randomized instruction from an A/B test, a serializer that does not order JSON keys the same way twice. Nothing errors. The bill simply stops improving. It also means this saving never appears on the hit-rate chart of step 6, because it lands entirely on the requests you counted as misses — give cached input tokens their own line, or you will conclude the prefix cache did nothing and rip it out.

  • reuse KVskip prefill on the prefix
  • stable prefix firstmaximize the shared span
  • helps on missescheaper even when uncached

Step 5 · Keep it fresh

TTL, invalidation, and versioning

A cache that never forgets eventually lies: the underlying document changed, a policy updated, the model was upgraded — but the old answer sits there, served forever. How do you keep a cache from going stale?

Response Storecached answersTTL / Invalidatefreshness
New in this step: TTL / Invalidate.

What keeps cached answers from becoming quietly wrong over time?

  1. Inputs change: docs get edited, prices update, models improve. Without expiry or invalidation the cache serves yesterday’s (now-wrong) answer indefinitely.

  2. Flushing everything throws away the hit-rate you worked for. You want targeted expiry and invalidation, not a nuclear reset.

  3. Each entry gets a TTL; when its source (a retrieved doc, a policy) changes you invalidate the affected entries; and the cache key includes the model version so an upgrade naturally partitions old from new.

Give every entry a TTL, invalidate entries whose inputs change (a document edited, a config updated), and version the key by model so a model upgrade doesn’t serve pre-upgrade answers. Scope keys per tenant. Now the cache stays fast and honest — freshness is a policy, not an accident.

Why this piece earns its place

Invalidate on input change is easy to say and needs a data structure nobody draws at the whiteboard: a reverse index from source to cache keys, written in the same operation that writes the entry. If you never recorded that this answer was built from document 41, you cannot find it when document 41 changes, and your options collapse to a flush or waiting out the TTL — which is the flush you promised not to do, just slower. Write the dependency down at cache-write time, or you do not have invalidation, you have expiry. Then jitter the TTL. The single-flight lock from step 1 protects one key against a stampede; this is the same failure spread across thousands of keys at once, where a per-key lock buys you nothing — entries written together expire together, so prompts cached during one traffic spike all come due in the same second. A few percent of randomness per entry is what breaks the correlation. And expiry is usually lazy: an entry past its TTL still occupies memory until a read or a sweep finds it, so capacity is planned against entries stored, not entries still live.

  • TTLbounded staleness
  • invalidateon source change
  • version keyby model+version

What the new pieces do

TTL / Invalidateservice
Expires entries by TTL and invalidates them when their inputs change (a document updated, a model upgraded), so the cache never serves a stale or wrong-version answer.

Step 6 · Prove it helps

Hit-rate, savings, and false-hit alarms

You’ve added two cache tiers, a prefix cache, TTLs and a threshold. Is any of it actually helping — and is the semantic tier quietly hurting? You can’t tell without measurement. What do you track?

Cache APIExact CacheEmbedderSemantic IndexLLM ProviderHit-rate & $
New in this step: Hit-rate & $. · swipe to pan the diagram

What metrics tell you the cache is working (and not lying)?

  1. Request volume says nothing about whether the cache saved money or served wrong answers. You need hit-rate, cost delta, and a false-hit signal.

  2. Hit-rate per tier shows reach, cost/latency saved shows value, and sampling semantic hits for correctness catches a threshold that’s gone too loose — all attributed per tenant.

  3. Storage size is an operational detail, not a measure of value or correctness. The important numbers are hits, savings, and false hits.

Emit hit-rate per tier, cost and latency saved, and a false-hit signal (sample semantic hits and verify) — attributed per tenant. This proves the cache’s value and alarms when a loosened threshold starts serving wrong answers. Warm the cache for known-hot prompts so hit-rate is high from the first request.

Why this piece earns its place

Cost saved is the number that gets quoted upward, and you cannot actually measure it — you are counting money you did not spend. The only defensible way to compute it is to record the completion’s input and output token counts on the entry when you write it, then multiply by hits. Skip that and the savings figure is assembled after the fact from averages, and the first finance question takes it apart. Be careful using hit-rate as an alarm, too. Its denominator is your traffic mix, so it moves when one customer changes behavior and sits flat when the cache quietly breaks; cost per request is the better page signal, because it moves for one reason. The false-hit sample needs its own budget line, since judging whether a semantic hit answered the right question costs a person or a second model call — so fix a rate and pay it continuously. Then spend it unevenly: sample hardest just above the threshold, where the marginal matches live. A uniform sample across all hits spends most of its checks on near-identical prompts nobody doubts, and almost never sees the ones that decide whether the dial is set right.

  • hit-rateper tier
  • $ + ms savedthe value
  • false-hitthe alarm

What the new pieces do

Hit-rate & $store
Tracks hit-rate, cost saved, latency, and false-hit signals per tenant — the proof the cache is helping and the alarm when a loose threshold starts hurting.

Step 7 · Guard against drift

Embedder upgrades and index migration

Six months in, a better embedding model ships. Swap it in and the semantic cache’s existing vectors were computed by the OLD model — comparing a new-model query embedding against old-model cached embeddings isn’t "less accurate," it’s comparing two different coordinate systems. Nothing crashes; similarity scores just become meaningless. How do you upgrade the embedder without breaking the cache?

ApplicationCache APIExact CacheEmbedderSemantic IndexResponse StoreLLM ProviderTTL / InvalidateHit-rate & $
The system as it stands at this step. · swipe to pan the diagram

You’re upgrading the embedding model behind the semantic cache. What has to happen to the existing index?

  1. Distances between an old-model vector and a new-model vector aren’t meaningful — the ANN index would return "nearest" neighbors that aren’t actually near anything. Every semantic hit becomes untrustworthy at once.

  2. Either re-embed the existing cached prompts with the new model to rebuild the index, or run both embedders in parallel for a migration window until the new index has enough coverage — then cut over and retire the old one.

  3. Valid but expensive — you lose 100% of your accumulated hit-rate on day one. Re-embedding existing prompts (cheaper than re-querying the LLM) preserves most of the value.

Treat an embedder upgrade as a migration, not a config flip: either re-embed the existing cached prompts under the new model to rebuild the index, or dual-write to both old and new indexes for a transition window, cutting reads over once the new index has real coverage. The Semantic Index is only meaningful when every vector in it — cached and query-time — came from the same embedder version.

Why this piece earns its place

Two costs decide how this migration goes, and neither one is the re-embedding. The first is memory. A new embedder usually has a different vector dimension, so you cannot upsert into the existing index — you stand up a second one, and for the whole dual-write window you pay to hold both. That is a ceiling on how long the window can affordably run, not a date to end it on: if the new index will not have earned its coverage before that budget runs out, the move is to buy the coverage directly by re-embedding your hottest cached prompts up front, not to cut reads over early. The second cost is the threshold. A cutoff tuned in the old model’s space does not carry over: the new model has its own score distribution, and may use a different similarity metric or normalization, so the same number is a different strictness. Re-tune it against the new index before you send read traffic there, using the score log from step 3 — otherwise you execute a clean migration and land on either a cache that hits nothing or one that serves the wrong answer, and both read as the migration failed. Keep the old index readable until that number has settled.

The payoff

You built an LLM cache

From "pay full price for every answer" to a layered cache: cache-aside control flow, an exact-match tier on a normalized key, a semantic tier with a tuned threshold, prompt-prefix (KV) reuse to cheapen even misses, TTL + invalidation + model-versioning for freshness, and hit-rate/savings metrics that keep it honest.

ApplicationCache APIExact CacheEmbedderSemantic IndexResponse StoreLLM ProviderTTL / InvalidateHit-rate & $
The finished design, end to end. · swipe to pan the diagram

Now loosen the match — drop the semantic threshold — and watch the cache answer the wrong question at a great hit-rate: "cancel" served the "upgrade" answer, instantly, as a valid response, no error. That’s why the threshold is a correctness dial, why you sample semantic hits for false positives, and why a clean miss beats a confident wrong hit.

Everything you assembled, in order

  • Cache-aside — look up first; on a miss call the model and populate
  • Exact cache — hash the normalized prompt+model+params — byte-identical, always safe
  • Semantic cache — embed + ANN + threshold catches near-duplicates
  • Threshold — the dial between hit-rate and correctness — set it conservatively
  • Prefix (KV) cache — reuse the shared prompt prefix to cheapen even misses
  • Freshness — TTL, invalidate on input change, version the key by model
  • Metrics — hit-rate + savings + false-hit alarm, per tenant
  • The failure — a loose semantic threshold serves wrong answers silently

Deep cut · 31:22

Nothing crashed. The dashboard said zero errors.

The interactive build above lays out the cache: an exact-match tier keyed on a hash, a semantic tier searched by embedding similarity, TTL and invalidation, and prompt-prefix reuse. This film is an autopsy. A support bot answers “how do I cancel?” with the steps for upgrading, instantly, with zero errors, and the film rebuilds the cache one reasonable decision at a time, running the lookup line by line as pseudo code, until it finds the one line that served the wrong answer.

  • See the wrong hit: “cancel” and “upgrade” sit in the same topic on the shelf, score 0.86, and clear a threshold of 0.80, so a confident wrong answer is served looking exactly like a right one.
  • Take it into the interview: set the threshold by measuring false hits, not by feel, key the cache per tenant, expire answers the business can change, and price a miss with prefix caching.

Where an interviewer pokes next

Getting the boxes right is the easy half. These are the questions that separate a candidate who drew the diagram from one who has run the thing. Answer each one out loud before you open it.

  1. During an embedder migration you’re dual-writing to old and new indexes. How do you know when it’s safe to drop the old one?

    When the new index’s hit-rate on live traffic has stabilized near the old index’s historical rate — not on a fixed timer. Cutting over too early means the new index hasn’t accumulated enough cached prompts yet and hit-rate craters; the migration window should be coverage-driven (has the new index seen roughly the same prompt distribution the old one built up over its lifetime), not a fixed number of days.

  2. The prefix (KV) cache can’t hold every shared prefix forever — GPU memory is finite. What determines what gets evicted?

    Standard cache eviction pressure — typically LRU (least-recently-used) at the prefix level, so a system prompt still being hit every second stays resident while one from an hour-old, now-idle conversation gets reclaimed first. The eviction unit matters too: evicting whole prefixes cleanly (not partial KV state) keeps a prefix either fully cached or fully absent, avoiding a corrupted partial hit.

  3. Two requests have identical prompt text but different tool/function definitions attached. Same exact-cache key?

    No — tool definitions change what the model is even allowed to do in response, so they belong in the key alongside model and sampling params. A response cached without a tool call, served to a request that expected the model to be ABLE to call a tool, is a silent capability regression even though the visible text might look fine.

  4. How would you actually verify a prompt is "safe to cache generically" rather than assuming it based on wording?

    Check whether the RESPONSE (not just the prompt) contains any user-specific fields — order IDs, names, account data — by scanning the answer against known PII/entity patterns or against the request’s known user context before writing to cache. A prompt like "what’s my order status" looks generic, but its answer never is; the classification has to happen on what got generated, not on how the question was phrased.

  5. Can a cache hit still be streamed back so it feels the same as a fresh generation to the client?

    Yes — the Cache API can chunk a stored response and emit it over the same SSE/streaming protocol the client already expects, often even faster than real generation since there’s no token-by-token wait. Some teams deliberately pace a cached stream to roughly match generation speed, purely so a cache hit doesn’t look suspiciously instantaneous to users who might infer something about how their data is handled.

Check yourself — the answers, and why

Eight steps in, these are the calls you should be able to make cold. Pick one, then read why.

  1. An LLM cache is high-leverage because…

    Repeated and near-duplicate prompts are common; serving them from cache costs ~nothing and returns in milliseconds.

  2. A safe exact-cache key is a hash of…

    Model and params change the answer, so they belong in the key; normalize first so trivial formatting differences still hit.

  3. The semantic cache reuses an answer when…

    Embed the prompt, ANN-search cached prompts, and reuse a hit above a threshold — the dial between reach and correctness.

  4. Prompt-prefix (KV) caching helps because it…

    A long shared system prompt / context is the expensive part; caching its KV state skips prefill so only the new suffix is processed.

  5. To keep a cache from going stale you…

    Bounded TTLs, targeted invalidation, and model-version in the key keep the cache fast and honest without nuking the hit-rate.

  6. The semantic cache’s signature failure is…

    Loosening the threshold raises hit-rate but starts matching prompts that only seem similar — wrong answers, returned instantly with no error. Sample hits and re-verify.

How you’d open this design in an interview

Before any boxes: agree what it must do, pin the qualities that shape everything, then build — naming each trade-off as you make it. The walkthrough above is that exact order.

What it must do

Agree on these before drawing a single box.

  • Cache-aside: check for a stored answer before calling the model; on a miss, call and populate.
  • Exact match: hash the normalized prompt + model + params — a byte-identical hit, always safe.
  • Semantic match: embed the prompt and reuse a near-duplicate’s answer above a similarity threshold.
  • Prefix reuse: cache the model’s computed KV state for a shared prompt prefix, cheapening even misses.
  • Freshness + proof: TTL/invalidate/version entries, and track hit-rate, savings and false hits per tenant.

The qualities that shape everything

Each one names the mechanism that buys it.

The highest-leverage cost and latency cut
Cache-aside in front of the model — a hit skips an expensive, slow call and returns in milliseconds; the cheapest call is the one you never make.
Safe reuse of an identical request
Key the exact cache on a hash of the normalized prompt + model + params, so a hit is byte-identical and trivially safe to return verbatim.
Catch reworded-but-equivalent prompts
Embed the prompt, ANN-search cached-prompt embeddings, and reuse a hit above a tuned similarity threshold — meaning, not spelling.
Cheaper even on a cache miss
Prompt-prefix (KV) caching stores the transformer’s computed state for a shared prefix, so a request skips prefill on those tokens and only processes the new suffix.
Never serve a stale or wrong-version answer
TTL every entry, invalidate when its inputs change, and version the key by model — freshness as a policy, not an accident.
Keep speed from silently costing correctness
Track hit-rate per tier, cost + latency saved, and a false-hit signal per tenant — the alarm when a loosened threshold starts serving wrong answers.

The trade-offs you say out loud

Senior signal isn’t the boxes — it’s naming what you gave up and why it was the right price.

Cache-aside (look up first) over call the model, store after

Storing without checking first means you pay full price on every request; the lookup has to come before the model call so an identical request is free next time.

Key on prompt + model + params over the prompt text alone

The same prompt at a different temperature or model gives a different answer; folding model and sampling params into the key is what makes a hit byte-identical and safe.

Embedding + ANN similarity over string normalization

Lowercasing and stripping punctuation never aligns real synonyms ("cancel" vs "end my subscription"); embedding the prompt matches meaning — behind a tuned threshold.

Prefix (KV) caching over shortening the prompt

Trimming the system prompt hurts quality and still recomputes what remains; caching the KV state keeps the full prompt but stops paying to reprocess the shared prefix.

TTL + targeted invalidation over flushing the whole cache on any change

A nuclear flush throws away the hit-rate you earned; per-entry TTLs, source-change invalidation, and model-versioned keys keep the cache fresh without resetting it.

The answer, out loud

What a strong answer to “Design an LLM Cache” sounds like, first question to last trade-off. It is about 7 minutes of talking; the whiteboard and the interviewer fill the rest of the 45. Read it aloud once, then close the page and give it yourself.

  1. 0–3 min

    Pin down what we’re caching

    Before I draw anything I want to scope it. We’re putting a cache between an application and a language model, so what I’m buying back is money per call and seconds of latency — the model call is the most expensive and slowest hop in the request. It’s worth building because production prompt traffic is far less varied than it looks: questions cluster hard, and most of what gets sent is the same instructions wrapped around a slightly different sentence. So my target is reuse, and the constraint I’d put on the table first is that reuse has a sharp edge — a wrong stored answer comes back instantly, well formatted, with no error anywhere. That is what I want the design to protect against, not just hit-rate.

  2. 3–7 min

    Look up first, then call the model

    The skeleton is cache-aside, also called read-through. The application calls a cache API with the completion contract it already uses; on a hit it gets the stored answer, and on a miss the API calls the model, writes the result back, then returns. Calling the model every time and storing afterward saves nothing, and precomputing answers is impossible — the prompt space is effectively infinite — so I cache what actually gets asked. The application never learns whether an answer was cached.

    Built in step 1: Cache-aside around the model
  3. 7–13 min

    The exact tier, and what goes in the key

    The first tier is an exact-match cache keyed by a hash — and the design question is what goes in it. Prompt text alone is wrong: the same prompt on another model or temperature gives a different answer. So I normalize the request — trim whitespace, sort the JSON keys — and hash the prompt with the model, sampling parameters and tool definitions. An answer that can hold someone’s private data also needs the tenant or user in the key, or isn’t cached at all. A hit is byte-identical and safe; the cost is that it only catches literal repeats.

    Built in step 2: Exact-match cache on a normalized key
  4. 13–21 min

    Catch the rewordings, carefully

    Most repetition isn’t literal: “How do I cancel?” and “How can I cancel my plan?” are different bytes. Lowercasing never aligns real synonyms, and asking the model whether two prompts match spends the money I’m saving. So the second tier is semantic: an embedder turns the prompt into a vector — numbers where similar meanings land close together — and an approximate nearest-neighbor index finds the closest cached prompt. If similarity clears a threshold, I reuse its answer. That threshold is a correctness dial. Too tight misses real duplicates; too loose and “cancel” can score 0.86 against “upgrade,” clear 0.80, and get the upgrade steps. So I’d set it from measured false hits, and re-verify a borderline match before its answer goes out.

    Built in step 3: Semantic cache with a similarity threshold
  5. 21–27 min

    Make even a miss cheaper

    Even a miss can be cheaper. Most requests share a long, identical prefix — system prompt, examples, a retrieved document — then a short unique question. Trimming the prompt hurts quality and still recomputes the rest. Instead, prompt-prefix caching: the provider or our inference server keeps the model’s computed key-value state for that prefix, so a new request skips the prompt-processing pass, called prefill, on those tokens. That cuts cost and time-to-first-token — how long before the user sees anything — even on a response miss. It needs stable content first and the question last, and idle prefixes get evicted because GPU memory is finite.

    Built in step 4: Prompt-prefix (KV) caching on a miss
  6. 27–32 min

    Keep it fresh without flushing it

    A cache that never forgets eventually lies — the document changed, the policy updated, the model was upgraded. Flushing everything on any change throws away the hit-rate I earned. So every entry gets a time-to-live, or TTL; entries are invalidated when their source changes; and the key carries the model version, so an upgrade separates old answers from new. Staleness is the tax on any cache, so I make it explicit: short TTLs for volatile content.

    Built in step 5: TTL, invalidation, and versioning
  7. 32–37 min

    An embedder upgrade is a migration

    The subtler one arrives the day someone ships a better embedder. Nothing breaks loudly — the index still returns a nearest neighbor with a score that looks like every other score, because the vectors being compared came out of different models and don’t share a space. So I’d make that assumption explicit instead of cultural: stamp the index with the embedder version, and have a query carrying any other version refuse rather than answer. Then the upgrade is a migration — re-embed the stored prompts, or run both indexes while the new one fills — and I’d re-tune the threshold against the new index’s own score distribution before it takes read traffic, because the old cutoff means something different over there.

    Built in step 7: Embedder upgrades and index migration
  8. 37–42 min

    What I’d watch, and how it fails

    For the dashboard I’d separate the two questions the cache raises. Is it paying for itself — hit-rate split by tier, next to the money and the milliseconds it took off each request. And is it still honest — a sample of semantic hits, checked, reported as a rate. I’d break all of it out per tenant, because an aggregate hides the shape I care about: one heavy customer can hold the average flat while a smaller one is served wrong answers all day. And I’d name the number I wouldn’t celebrate on its own: hit-rate rises when the cache gets better and it rises when the threshold gets sloppy, so I read it next to the false-hit rate or I don’t read it at all.

    Built in step 6: Hit-rate, savings, and false-hit alarms
  9. 42–45 min

    Close on the trade-off

    To close, the design in one breath: cache-aside in front of the model, an exact tier on a normalized key, a semantic tier behind a tuned threshold, prefix caching for misses, TTLs and versioned keys for freshness, and metrics that count false hits. Every knob trades hit-rate against correctness. With more time I’d go to deciding what’s safe to cache for everyone — checking the generated answer for user data, not the question’s wording — and streaming cached answers so a hit feels fresh.

What this teaches

Learn AI system design by building a prompt & response caching layer for LLMs step by step. An interactive guide to a two-tier cache — exact-match hashing, semantic (embedding) lookup with a tuned similarity threshold, TTL and invalidation, server-side prompt-prefix (KV) reuse, and hit-rate economics — so repeated and near-duplicate prompts skip the model and cut cost and latency.

Key takeaways

  • Cache-aside — look up first; on a miss call the model and populate
  • Exact cache — hash the normalized prompt+model+params — byte-identical, always safe
  • Semantic cache — embed + ANN + threshold catches near-duplicates
  • Threshold — the dial between hit-rate and correctness — set it conservatively
  • Prefix (KV) cache — reuse the shared prompt prefix to cheapen even misses
  • Freshness — TTL, invalidate on input change, version the key by model
  • Metrics — hit-rate + savings + false-hit alarm, per tenant
  • The failure — a loose semantic threshold serves wrong answers silently

Concepts covered

  • Why cache LLM calls at all?
  • Cache-aside around the model
  • Exact-match cache on a normalized key
  • Semantic cache with a similarity threshold
  • Prompt-prefix (KV) caching on a miss
  • TTL, invalidation, and versioning
  • Hit-rate, savings, and false-hit alarms
  • Embedder upgrades and index migration
RUN IT YOURSELF

The threshold that serves someone else’s answer

Both cache tiers of this page as one runnable file — hash the canonicalized request, then embed it and reuse the nearest cached prompt above a threshold — running for real in your browser. It sweeps that threshold over labelled traffic and prints hit rate and false-hit rate side by side, so you can watch one buy the other. Move the sweep in range(9), or add your own question to QUERIES, and hit Run.

HOW TO READ THE CODE — 4 IDEAS
  1. The exact tier (step 2) hashes the normalized prompt with the model, params and tenant: it is never wrong, it cannot leak across tenants, and it catches 1 request in 14.
  2. The semantic tier (step 3) matches meaning — can I have my money back scores 0.843 against How do I get a refund? with no content word in common.
  3. A false hit is a served answer from an entry that answers a different question. No error, no log line — that is the failure this page’s chaos button triggers.
  4. The printed frontier is the argument (step 6): 57% of requests can be served, but only 43% with none wrong, because right and wrong top-1 scores overlap between 0.580 and 0.813.
CPython · WebAssembly
built to be reasoned about, not memorized — make the calls, loosen the match, run the quiz.
Finished this one? 0 / 61 AI System Designs done

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More AI System Designs