Skip to content
intermediate Phase 17 · Serving Patterns for AI Applications

Caching Model Responses

Cache deterministic results with semantic keys and invalidate on prompt changes.

50m
0 problems
Topic Progress 0%

Exact-Match Caching: Hash the Whole Request

Model calls are the most expensive operation in an AI backend: 1-15 seconds and real money per request. A surprising share of production traffic is the same question asked repeatedly — status checks, policy questions, 'what are your hours?' A cache turns those repeat questions into sub-millisecond lookups.

Exact-match caching is the simplest form: if the identical request was answered recently, return the stored answer. The subtle part is the cache key. Hashing only the prompt text is a bug waiting to happen:

import hashlib
import json
import time

TTL = 3600
cache = {}  # production: Redis with EXPIRE

def cache_key(request: dict, prompt_version: str) -> str:
    # canonical JSON: sorted keys so key order does not change the hash
    canonical = json.dumps(request, sort_keys=True)
    return hashlib.sha256(f'{prompt_version}:{canonical}'.encode()).hexdigest()

def complete(request: dict, prompt_version: str) -> str:
    key = cache_key(request, prompt_version)
    hit = cache.get(key)
    if hit and hit['expires'] > time.time():
        hit['hits'] += 1
        return hit['answer']  # skip the provider call entirely
    answer = call_model(request)
    cache[key] = {'answer': answer, 'expires': time.time() + TTL, 'hits': 1}
    return answer
complete({'model': 'gpt-4o-mini', 'messages': [...], 'temperature': 0}, 'v3')
--> miss: provider call, 1.2s, $0.0004
complete(same request, 'v3')
--> hit: 0.3ms, $0.0000
complete(same request, 'v4')        # prompt was edited
--> miss: different key, fresh generation

The key must cover everything that changes the answer: model, messages, temperature, tool definitions, and the prompt version. Miss any of them and you serve an answer computed under different assumptions — a correctness bug, not just a stale one. json.dumps(..., sort_keys=True) matters more than it looks: {'a':1,'b':2} and {'b':2,'a':1} are the same request, and without sorted keys they hash differently, silently halving your hit rate.

The economics are worth stating plainly. At 10,000 requests per day with a 40% hit rate, you skip 4,000 provider calls daily. At token costs of a fraction of a cent per call, that is the difference between a feature that pays for itself and one that bleeds.

Semantic Caching and Its Staleness Risk

Exact matching fails on the long tail: 'What is your return policy?' and 'How do I send something back?' are the same question with different words. Semantic caching answers with embeddings — store the question's vector, and on a new question, retrieve the nearest past question and reuse its answer if the similarity is high enough.

def semantic_complete(query: str, prompt_version: str) -> str:
    q_vec = embed(query)
    cached = vector_db.search(q_vec, top_k=1)
    if cached and cosine(q_vec, cached.vec) > 0.97:
        # near-identical question already answered — but only trust it if
        # it was generated under the current prompt version
        if cached.prompt_version == prompt_version:
            return cached.answer
    answer = call_model(query)
    vector_db.upsert(q_vec, answer, prompt_version)
    return answer
embed('What is your return policy?')   -> vec A
embed('How do I send something back?') -> vec B
cosine(A, B) = 0.981 > 0.97 -> cache hit, answer reused

The threshold is the whole game. At 0.90 you get a high hit rate and serve wrong answers to questions that merely look similar. At 0.99 you get almost no hits. Tune it on your own queries — this is a domain-specific knob, not a universal constant, and the same discipline applies as when evaluating embeddings.

The staleness risk is real and specific. A semantic hit means 'a similar question was answered before', not 'this answer is still true'. If the return policy changed last week, the cached answer is confidently wrong. Two mitigations: the prompt-version gate in the code above, and a shorter TTL for semantic entries than exact ones. Semantic caching is a cost optimization with an error budget — use it for traffic where a slightly stale answer is acceptable, and keep exact matching for anything factual.

Invalidation: Prompt Versions and TTLs

You ship a prompt edit on Friday. On Saturday, users report the old wording in answers. The cache is serving pre-edit responses, and nobody included the prompt version in the key. This is the most common caching bug in AI systems, and it is entirely preventable.

Invalidation in an AI cache works on two axes: identity and time.

Identity means the key must contain everything that defines the answer — including the prompt version, as in the code above. When the prompt changes, the key changes, old entries become unreachable garbage, and they expire on their own. No flush endpoint, no cache-wide invalidation job, no race between deploy and purge.

Time means the TTL must match how fast the underlying truth changes:

Content type Example Sensible TTL
Reference facts 'What is our return policy?' 7 days
Product answers 'Compare the Pro and Team plans' 1 day
Session summaries rolling conversation summary session length
Live data 'Is item X in stock?' 60 seconds, or do not cache
Personalized answers 'What did I ask last week?' per-user, short

The live-data row is the one that bites. Caching a stock answer for 60 seconds is a defensible trade; caching it for an hour is a support ticket. When the answer depends on state you do not control, either skip the cache or make the TTL shorter than the state's change interval.

There is a subtler failure: the version gate only works if the version actually changes. If your prompt pipeline builds the system prompt from ten components, the cache key must hash the assembled prompt or a version of the whole assembly — a version bump to one component that does not reach the key leaves stale answers in place.

Failure Modes and Cache Stampedes

Caching an AI backend introduces failure modes that have nothing to do with the model. The ones below are the ones that actually reach production.

Failure Symptom Fix
Thundering herd hot key expires; 500 concurrent misses all call the provider request coalescing: one in-flight call per key, others await it
Cache poisoning a prompt-injection answer gets cached and served to others sanitize before storing; never cache unvalidated output
Unbounded growth keys accumulate forever; memory climbs max size with LRU eviction, or Redis maxmemory policy
Semantic false hit similar question, wrong answer served tune threshold on your data; version-gate semantic entries
Stale after deploy old answers served after a prompt change prompt version in the key

The thundering herd deserves the most engineering attention. When a popular entry expires, every concurrent request misses simultaneously and they all dispatch provider calls — the cache amplifies the load spike it was built to absorb. The fix is request coalescing: the first miss for a key becomes the caller, and concurrent requests for the same key wait on that call's result instead of starting their own. Memcached and Redis both have primitives for this (SET NX as a lock), and it is twenty lines of code.

The poisoning row is specific to AI systems. A cached answer is content you will show users without a model in the loop. If one user's prompt injection gets cached under a semantic or exact key, every subsequent match receives it. Validate and sanitize before storing, treat the cache as an output surface, and remember that caching interacts with cost budgeting: a high hit rate is a cost win, but only if the hits are correct. Measure hit rate and answer quality together, the same way you would for online eval metrics.

Quiz

1. Why must an exact-match cache key hash the full request rather than just the prompt text?

Question 1 options

2. What is the main risk of semantic caching compared to exact-match caching?

Question 2 options

3. Why does including the prompt version in the cache key eliminate stale answers after a prompt edit?

Question 3 options

4. What is a thundering herd in the context of response caching?

Question 4 options

Flashcards

Question

What belongs in an exact-match cache key for model responses?

Answer

Everything that changes the answer: model, messages, temperature, tool definitions, and prompt version — hashed as canonical JSON so key order does not affect the hash.

Question

Why is hashing only the prompt text a bug?

Answer

The same prompt with a different model or sampling parameters produces a different answer. A text-only key serves an answer computed under different assumptions — a correctness bug, not just staleness.

Question

What does semantic caching trade for a higher hit rate?

Answer

Accuracy. A semantic hit means a similar question was answered before, not that the answer is still true — so it needs a tuned similarity threshold, version gating, and shorter TTLs.

Question

How does a prompt version in the cache key handle invalidation?

Answer

Editing the prompt bumps the version, which changes every key. Old entries become unreachable and expire naturally — no flush endpoint or race between deploy and purge.

Question

What is request coalescing and when is it needed?

Answer

Allowing only one in-flight provider call per cache key while concurrent requests await its result. Needed when a hot key expires under load, to prevent a thundering herd of simultaneous provider calls.

Revision Notes

Key Takeaways

  • 1. Hash the full request — model, messages, temperature, tools, prompt version — because any of them changes the answer
  • 2. A prompt version in the key makes invalidation automatic: edits change the keys, old entries expire, no flush job needed
  • 3. Semantic caching buys hit rate with an error budget: tune the threshold on your data and version-gate entries
  • 4. TTL must match how fast the underlying truth changes; live data gets seconds or nothing
  • 5. Coalesce concurrent misses on the same key, or a hot key expiry becomes a provider-side spike

Interview Tips

  • • Lead with the economics: hit rate x cost per call = dollars saved, then pivot to the correctness subtlety that full-request keys are about wrong answers, not stale ones
  • • The prompt-version question is a favorite follow-up — have the 'version in the key, no flush job' answer ready
  • • Mention the thundering herd and request coalescing; it separates people who have run a cache from people who have read about one

Cheat Sheet

Caching Model Responses Cheat Sheet

Exact-match key:

canonical = json.dumps(request, sort_keys=True)  # sorted: key order must not matter
key = sha256(f'{prompt_version}:{canonical}')

Semantic cache:

cached = vector_db.search(embed(query), top_k=1)
if cached and cosine(q_vec, cached.vec) > 0.97 and cached.prompt_version == version:
    return cached.answer

TTL by volatility:

Content TTL
Reference facts 7 days
Product answers 1 day
Live data 60s or none
Session summaries session length

Failure modes: thundering herd (coalesce misses) · poisoning (sanitize before storing) · unbounded growth (LRU/maxmemory) · semantic false hits (tune threshold) · stale after deploy (version in key)

Rules of thumb:

  • hash the FULL request — model, params, tools, prompt version
  • version in the key = free invalidation
  • semantic entries: shorter TTL + version gate
  • measure hit rate AND answer quality together

Continue Learning

Before You Start

  • No prerequisites listed.

Related Reading