Skip to content

All lessons

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

  1. Run Redis: a managed service in production, or a local container; your app connects with a client library (redis for Python, with an asyncio API).
  2. 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.
  3. 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).
  4. Cache-aside: GET key → on a hit return it; on a miss compute, SET key value EX ttl, return (see the code).
  5. Choose TTLs: long for embeddings (until the embedding model changes), short for answers that depend on changing data.
  6. 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.
  7. Measure: hit rate, latency saved, cost saved, memory use; set an eviction policy (e.g. least recently used) for when memory fills up.
  8. Know the neighbours: exact caching here; semantic caching matches similar questions; provider prompt caching reuses prompt prefixes inside the model. They stack.
Loading the playground…

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.

redis_cache.py
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"))
Output
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: 1

Four 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.

  1. 01In the cache-aside pattern, what happens on a cache miss?
  2. 02What must the cache key for an LLM answer include?
  3. 03Why set a TTL on cached entries?
  4. 04Which is an especially good thing to cache in a RAG app?
  5. 05Why is Redis fast?

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.