FastAPI Caching with Redis
Some answers take real effort to produce: a heavy database query, a call to a slow partner service, a long calculation. When hundreds of visitors ask the same question, repeating that effort wastes time and money. A cache stores the finished answer in fast memory so the next visitor receives it instantly. Think of a chef who cooks a big pot of soup at noon. Each guest gets a ladle from the pot rather than a fresh bowl cooked from scratch.
What Redis Is
Redis is an in-memory data store. It keeps data in RAM, which makes reads and writes extremely quick. Redis stores simple key and value pairs, and each pair can carry an expiry time.
key value expires in
───────────────────── ─────────────────────────── ──────────
product:42 {"id": 42, "name": "Lamp"} 60 seconds
top-sellers [12, 7, 42, 19] 300 secondsStarting Redis Locally
Docker offers the quickest way to run Redis on a laptop.
docker run -d --name my-redis -p 6379:6379 redis
pip install redisThe Cache-Aside Pattern
Most APIs follow one simple recipe called cache-aside. The route checks the cache first and visits the database only after a miss.
Request arrives
│
▼
Look in Redis ──── found (hit) ───────────────▶ return cached answer
│
not found (miss)
│
▼
Query the database
│
▼
Save the answer in Redis with an expiry time
│
▼
Return the answerConnecting from FastAPI
import json
import redis.asyncio as redis
from fastapi import FastAPI
app = FastAPI()
cache = redis.from_url("redis://localhost:6379", decode_responses=True)The option decode_responses=True returns plain strings instead of raw bytes.
Caching a Route
import asyncio
async def load_product_from_db(product_id: int):
await asyncio.sleep(2) # pretend the query takes two seconds
return {"id": product_id, "name": "Desk Lamp", "price": 35.0}
@app.get("/products/{product_id}")
async def get_product(product_id: int):
key = f"product:{product_id}"
cached = await cache.get(key)
if cached:
return json.loads(cached)
product = await load_product_from_db(product_id)
await cache.set(key, json.dumps(product), ex=60)
return product
Call 1: GET /products/42 ──▶ miss, database query ──▶ about 2 seconds
Call 2: GET /products/42 ──▶ hit, served from Redis ──▶ a few milliseconds
...
After 60 seconds the key expires, and the next call loads fresh data.The argument ex=60 tells Redis to delete the key after sixty seconds.
Keeping the Cache Fresh
Cached data can drift away from the truth. Two habits keep it honest.
Expiry Times
Short lifetimes limit how stale an answer can become. Pick the lifetime by asking how much delay the business can tolerate. A news headline list may live for one minute. A country list may live for a day.
Deleting on Change
Remove the cached copy whenever the source record changes.
@app.put("/products/{product_id}")
async def update_product(product_id: int, data: dict):
# ...save changes to the database...
await cache.delete(f"product:{product_id}")
return {"updated": True}Designing Good Keys
- Use a readable pattern such as
type:id. - Include every input that changes the answer, such as
products:page=2:size=10. - Add the user ID to keys that hold personal data.
Connection Management
Create the Redis client once and reuse it. Close it at shutdown through a lifespan function.
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app: FastAPI):
yield
await cache.aclose()
app = FastAPI(lifespan=lifespan)What to Cache and What to Skip
Good candidates Poor candidates
───────────────────────────── ─────────────────────────────
Popular product pages Bank balances
Lists that change rarely One-time passwords
Results of heavy reports Data unique to each request
Responses from slow partners Anything that must be liveCommon Mistakes
Caching Private Data Under a Shared Key
One user's profile served to another user is a serious leak. Put the user ID in the key.
Forgetting Expiry
Keys without a lifetime pile up and fill memory.
Trusting the Cache Blindly
Redis can restart and lose data. Treat the cache as a speed boost, and keep the database as the source of truth.
A Simple Speed Check
Measure the gain with a short test. Call the cached route twice and compare the response times in the interactive docs or with a command line tool.
curl -w "time: %{time_total}s
" -o /dev/null -s http://127.0.0.1:8000/products/42The first call reports about two seconds because the route reaches the slow data source. The second call reports a few thousandths of a second. The numbers show why large websites cache aggressively: a single slow query serves thousands of visitors after one calculation.
Key Points
- A cache stores finished answers in fast memory.
- Redis holds key and value pairs with optional expiry times.
- Cache-aside checks the cache first and fills it after a miss.
- Delete or expire entries so users never see old data for long.
- Build keys that include every input affecting the answer.
