Area 08 // BackendLesson 133 / 14013 min read
Caching with Redis
Redis is a very fast in-memory data store, and caching with it means saving the results of slow or expensive work (LLM answers, embeddings, database queries) under a key with an expiry time, so repeated requests are served in milliseconds without redoing the work.
Think of it like this
Analogy
A busy café makes the most popular drinks in batches and keeps them warm for a few minutes. If someone orders a flat white and one is ready, it's handed over instantly (a cache hit); if not, the barista makes a fresh one and keeps any extras warm (a miss, then store). After a while the leftovers are thrown away so nobody gets a cold coffee (TTL).
The problem it solves
Many requests in AI apps repeat the same expensive work: embedding the same document again, sending the identical prompt for a popular question, calling the same weather API for the same city, rerunning the same database query on every page load. Each repeat costs time (hundreds of milliseconds to seconds) and money. A shared, fast cache removes most of that.
How it works, step by step
- Run Redis: a managed service in production, or a local container; your app connects with a client library (
redisfor Python, with an asyncio API). - Choose what to cache: deterministic or slowly changing results: embeddings, exact repeated prompts (with care), retrieval results, external API responses, rendered pages, expensive database queries.
- Design the key: a prefix plus a hash of everything that affects the result: model, prompt version, normalised input, and the tenant or user if the result is personal (tenant isolation).
- Cache-aside:
GET key→ on a hit return it; on a miss compute,SET key value EX ttl, return (see the code). - Choose TTLs: long for embeddings (until the embedding model changes), short for answers that depend on changing data.
- Invalidate on changes: delete keys when source documents or prompts change, or include a version number in the key so old entries are simply never read again.
- Measure: hit rate, latency saved, cost saved, memory use; set an eviction policy (e.g. least recently used) for when memory fills up.
- Know the neighbours: exact caching here; semantic caching matches similar questions; provider prompt caching reuses prompt prefixes inside the model. They stack.
See it in code
- Install
pip install fakeredis- Needs
- Python 3.10+. fakeredis runs in memory; swap in redis.Redis(host=..., port=6379) for a real server.
Cache-aside for LLM answers with a one-hour TTL, plus a rate-limit counter.
import hashlib, json, time
import fakeredis # in-memory stand-in; in production: redis.Redis(host=..., port=6379)
r = fakeredis.FakeRedis(decode_responses=True)
CALLS = {"llm": 0}
def call_llm(prompt: str) -> str:
CALLS["llm"] += 1
time.sleep(0.2) # pretend the model takes a while
return f"answer to: {prompt}"
def cached_answer(prompt: str, model: str, ttl_s: int = 3600) -> tuple[str, str]:
key = "llm:" + hashlib.sha256(json.dumps([model, prompt]).encode()).hexdigest()[:16]
hit = r.get(key) # 1. look in the cache
if hit is not None:
return hit, "HIT"
answer = call_llm(prompt) # 2. miss: do the slow, costly work
r.set(key, answer, ex=ttl_s) # 3. store it with an expiry (TTL)
return answer, "MISS"
for prompt in ["What is RAG?", "What is RAG?", "What is an agent?", "What is RAG?"]:
start = time.perf_counter()
answer, status = cached_answer(prompt, "small-model")
ms = round((time.perf_counter() - start) * 1000, -2) # rounded to 100 ms for a stable demo
print(f"{status:4} ~{ms:3.0f} ms {answer}")
print("model calls:", CALLS["llm"], "| keys:", len(r.keys("llm:*")), "| ttl of one key:", r.ttl(r.keys("llm:*")[0]), "s")
# Other Redis jobs in AI apps: rate-limit counters, session state, queues, distributed locks.
r.incr("ratelimit:alice:2026-11-14T10:15")
r.expire("ratelimit:alice:2026-11-14T10:15", 60)
print("alice requests this minute:", r.get("ratelimit:alice:2026-11-14T10:15"))MISS ~200 ms answer to: What is RAG?
HIT ~ 0 ms answer to: What is RAG?
MISS ~200 ms answer to: What is an agent?
HIT ~ 0 ms answer to: What is RAG?
model calls: 2 | keys: 2 | ttl of one key: 3600 s
alice requests this minute: 1Four requests, two model calls. With real Redis over a network a hit takes about a millisecond, still hundreds of times faster than an LLM call.
A real example
Example
Re-embedding the same documents every night
A team's nightly ingestion job re-embedded 2 million chunks even though fewer than 1% changed each day. Adding a Redis cache keyed by embedding-model + sha256(chunk text) (no TTL, invalidated by changing the model name in the key) meant unchanged chunks reused their embeddings: the job went from 3 hours to 6 minutes and embedding spend fell by 99%. They also cached the top 500 FAQ answers per tenant for 10 minutes, cutting peak model calls by a fifth.
The easy way
Start here
Start with one cache: embeddings keyed by model and text hash, or responses from a slow external API. Use cache-aside with a TTL, and log hits and misses so you can see whether it's worth it.
When you grow: the production setup
Teams run managed Redis with replication, use versioned key prefixes, choose eviction policies and memory limits, protect against cache stampedes (many misses at once) with locks or early refresh, scope personal data by tenant, avoid caching anything sensitive without encryption and short TTLs, and track hit rates and savings on dashboards.
Trade-offs
What you gain
- Big latency and cost savings for repeated work.
- Simple to add with cache-aside.
- Redis also covers rate limits, sessions, queues and locks.
What it costs you
- Stale data if invalidation is wrong.
- Another service to run and secure.
- Memory is limited and more expensive than disk.
When not to use it
- Results that are unique every time or must always be fresh (live prices, personalised answers without tenant scoping).
Common mistakes
- Cache keys missing the model or prompt version.
- No TTL, so stale answers live forever.
- Sharing cached personal answers between users.
- Caching errors and serving them repeatedly.
- Treating Redis as the only copy of important data.
Interview questions
Try answering out loud first, then open the model answer.
What would you cache in an LLM application and how?
Model answer
Embeddings (keyed by model and text hash, long-lived), exact repeated prompts for non-personal answers (keyed by model, prompt version and normalised input, short TTL), retrieval results, tool and external API responses, and expensive queries, all via cache-aside in Redis with TTLs, versioned keys, tenant scoping and hit-rate monitoring; plus provider prompt caching for long stable prefixes.
How do you handle cache invalidation?
Model answer
Use TTLs as a safety net; delete or overwrite keys when source data changes (event-driven invalidation); include version numbers (data, prompt or model version) in keys so changes naturally bypass old entries; and avoid caching data whose freshness is critical.
What is a cache stampede and how do you prevent it?
Model answer
When a popular key expires, many requests miss at once and all hit the slow backend simultaneously. Prevent it with a lock so only one request recomputes while others wait or get the stale value, early probabilistic refresh before expiry, staggered TTLs, and request coalescing.
Quiz
Pick an answer to see whether it’s right and why.
0 of 5 answered
Key terms
- Redis
- A very fast in-memory data store used for caching, rate-limit counters, sessions, queues and locks.
- Cache
- A fast store of results that were expensive to compute or fetch, reused for repeated requests until they expire.
- Cache-aside
- The common caching pattern where the app checks the cache first, and on a miss computes the result, stores it with a TTL and returns it.
- TTL (time to live)
- How long a cached entry stays valid before it expires and must be fetched or computed again.
- Cache invalidation
- Removing or bypassing cached entries when the underlying data changes, so users don't get stale results.
Before and after
Progress is saved in this browser only.