Quick answer:
If you want fast, cheap look-ups for query results, session data, or computed values, add a Redis instance, connect with aioredis, wrap your endpoints in a small decorator, and let the cache expire or be cleared on writes. The whole stack fits in a few dozen lines and costs pennies per month on most cloud providers.
Below I walk through a production-ready implementation of redis caching in fastapi. I’ll show you how to install Redis, wire an async client, build reusable decorators, handle invalidation, spin the service up with Docker, and keep an eye on performance once you’re in the wild. I’ll also point out the places where things tend to break and what I would do differently next time.
Installing and configuring Redis for a FastAPI project
Do I need a separate Redis server?
Yes. Even a single-node instance gives you in-memory speed without adding latency to your API process. For local development you can run the official Docker image; for production you’ll want a managed service or a replicated cluster.
Local Docker setup
# docker-compose.yml
version: "3.9"
services:
redis:
image: redis:7-alpine
ports:
- "6379:6379"
command: ["redis-server", "--save", "60", "1", "--loglevel", "warning"]
restart: unless-stopped
Run docker-compose up -d redis. The container is tiny (≈30 MB) and starts in a second. On a VM or Kubernetes you would expose the service via a ClusterIP or a cloud-managed endpoint.
Production connection string
# .env
REDIS_URL=redis://:myStrongPassword@redis-prod.internal:6379/0
Never hard-code credentials. Keep the URL in a secret manager and inject it at runtime. I’ve been bitten by missing redis:// prefixes when moving from localhost to a managed instance - FastAPI will raise a ValueError at startup.
How do I integrate an async Redis client with aioredis?
Can I use the same client for all requests?
Yes, but you should create a single connection pool at startup and share it via FastAPI’s dependency system. aioredis (now part of redis-py v4) gives you native asyncio support.
# app/redis.py
import os
from redis.asyncio import Redis
from fastapi import FastAPI
def get_redis_pool() -> Redis:
return Redis.from_url(os.getenv("REDIS_URL"), decode_responses=True)
def add_redis(app: FastAPI) -> None:
@app.on_event("startup")
async def startup_redis():
app.state.redis = get_redis_pool()
# Test the connection early
await app.state.redis.ping()
@app.on_event("shutdown")
async def shutdown_redis():
await app.state.redis.close()
In any route you can now do:
from fastapi import Depends, Request
async def redis_dep(request: Request) -> Redis:
return request.app.state.redis
Trade-offs
- Single pool – reduces connection overhead but can become a bottleneck if you saturate the event loop with heavy Redis work. In that case, spawn a second pool for low-latency reads.
-
Blocking commands – avoid commands that return huge payloads; they block the event loop while the client decompresses data. Use
SCANinstead ofKEYSin production.
How can I build reusable cache decorators for FastAPI endpoints?
Is a decorator enough for all use cases?
For read-only or idempotent endpoints, a decorator is clean and keeps the view logic tidy. For write-heavy resources you’ll need explicit invalidation.
# app/cache.py
import functools
import json
from typing import Callable, Any
from redis.asyncio import Redis
def redis_cache(key_builder: Callable[..., str],
ttl: int = 300):
"""Cache the result of an async endpoint.
key_builder receives the same args/kwargs as the endpoint.
"""
def decorator(func: Callable):
@functools.wraps(func)
async def wrapper(*args, **kwargs):
request = kwargs.get("request")
redis: Redis = request.app.state.redis # type: ignore
cache_key = key_builder(*args, **kwargs)
cached = await redis.get(cache_key)
if cached is not None:
return json.loads(cached)
result = await func(*args, **kwargs)
await redis.set(cache_key, json.dumps(result), ex=ttl)
return result
return wrapper
return decorator
Example usage
# app/routers/items.py
from fastapi import APIRouter, Request
from .cache import redis_cache
router = APIRouter()
def item_key(request: Request, item_id: int):
return f"item:{item_id}"
@router.get("/items/{item_id}")
@redis_cache(key_builder=item_key, ttl=600)
async def get_item(request: Request, item_id: int):
# Simulate expensive DB call
return {"id": item_id, "value": "expensive computation"}
The decorator automatically reuses the pool created earlier, serializes the response with json, and respects the TTL.
When not to use it
- Streaming responses – you cannot cache a generator without buffering the whole payload.
- Very large payloads – they waste memory and network bandwidth. Consider pagination or a separate blob store.
What are the best cache invalidation and expiration strategies?
How do I keep stale data from leaking?
-
Time-based expiration (TTL) – simplest. Set a reasonable
ttlper endpoint. For data that changes every few minutes, a 60-second TTL is enough. - Write-through invalidation – after you write to the database, delete the related keys.
# app/routers/items.py
@router.put("/items/{item_id}")
async def update_item(request: Request, item_id: int, payload: dict):
# Update DB here …
await request.app.state.redis.delete(f"item:{item_id}")
return {"status": "updated"}
- Tag-based eviction – store a set of keys per logical group and delete the whole set on a bulk change.
# add tag on set
await redis.sadd(f"tag:user:{user_id}", cache_key)
# later invalidate all
keys = await redis.smembers(f"tag:user:{user_id}")
if keys:
await redis.delete(*keys)
await redis.delete(f"tag:user:{user_id}")
Pitfalls
-
Forgotten invalidation – I once added a new
PUTendpoint and missed thedeletecall. The cache kept returning the old version for hours. Adding a unit test that checks cache freshness saved me a lot of tickets. - Over-eager expiration – setting TTLs too low defeats the purpose of caching and adds load to Redis. Profile your read/write ratio before deciding.
How do I deploy Redis with Docker and handle environment-specific settings?
Can I keep the same compose file for dev and prod?
Yes, but you should separate the image version and resource limits. In production you may want persistence, ACLs, and monitoring side-cars.
Production-ready compose snippet
# docker-compose.prod.yml
version: "3.9"
services:
redis:
image: redis:7-alpine
command:
- redis-server
- /usr/local/etc/redis/redis.conf
volumes:
- redis-data:/data
- ./redis.conf:/usr/local/etc/redis/redis.conf:ro
environment:
- REDIS_PASSWORD=${REDIS_PASSWORD}
ports:
- "6379:6379"
deploy:
resources:
limits:
memory: 512M
reservations:
memory: 256M
restart: unless-stopped
volumes:
redis-data:
redis.conf can enable ACLs:
requirepass ${REDIS_PASSWORD}
maxmemory 256mb
maxmemory-policy allkeys-lru
When deploying to Google Cloud Run you cannot run a side-car, so you’ll point the FastAPI container to a managed Memorystore instance instead. The same REDIS_URL env var works for both setups.
What I’d do differently
-
Separate config repo – keep
redis.confin a dedicated folder, version it, and inject secrets via the orchestrator. I once edited the file inside the container and lost the change after a redeploy. -
Health checks – add a simple
redis-cli pinghealth endpoint in your orchestrator. Without it, a pod could start while Redis was still booting, causing a cascade of connection errors.
How can I monitor and troubleshoot Redis cache performance in production?
What metrics matter most?
Hit-rate, latency, memory usage, and evicted keys. Redis ships with INFO and the Redis Exporter for Prometheus.
Prometheus exporter
# docker-compose.yml (add beside redis)
redis-exporter:
image: oliver006/redis_exporter
environment:
- REDIS_ADDR=redis://:${REDIS_PASSWORD}@redis:6379
ports:
- "9121:9121"
Grafana dashboards can then show redis_keyspace_hits_total / (hits + misses) as a percentage. A dip below 80 % usually means your TTL is too short or you’re missing a cache-warming step.
Logging slow commands
Add to redis.conf:
slowlog-log-slower-than 10000 # microseconds
slowlog-max-len 128
You can query SLOWLOG GET from your FastAPI admin endpoint to see which queries are eating time. In one project I discovered a ZRANGEBYSCORE on a 10 M-element sorted set that took 250 ms; the fix was to add a secondary index.
Common failure modes
| Symptom | Likely cause | Quick fix |
|---|---|---|
ConnectionError: Timeout |
Redis container restarted or network partition | Check Docker logs, add restart: always, verify health probe |
| Cache always miss | Wrong key format or missing decode_responses=True
|
Ensure the same key builder is used for set and get |
| Memory limit reached |
maxmemory too low, or unbounded keys |
Enable LRU eviction, monitor used_memory_peak
|
FAQ
How many Redis nodes do I need for a small FastAPI service?
A single node is fine for low-traffic workloads. Add a replica once you exceed ~100 K requests per second or need high availability.
Can I use Redis as a message broker instead of a cache?
Technically yes, but Pub/Sub semantics differ from a durable queue. For background jobs I prefer RabbitMQ or Google Pub/Sub.
Is it safe to store user-session data in Redis without encryption?
If the data is not personally identifiable, plain storage is okay. For anything sensitive, enable TLS on the Redis endpoint and store only a session token that references server-side data.
What’s the cost difference between managed Memorystore and a self-hosted Docker container?
Managed services start at a few dollars per GB per month and include automatic failover. A self-hosted container on a small VM can be cheaper (< $5) but you’re responsible for backups and scaling.
Key Takeaways
- Install Redis as a separate service; use Docker locally and a managed instance in prod.
- Create a single async
Redispool at FastAPI startup and inject it via request state. - Wrap read-only endpoints in a lightweight decorator that builds keys, checks the cache, and sets a TTL.
- Invalidate on writes with explicit
deletecalls or tag-based sets for bulk eviction. - Deploy with environment-specific
redis.conf, enable ACLs, and set sensible memory limits. - Export metrics to Prometheus, watch hit-rate and latency, and enable the slow-log for troubleshooting.
With these pieces in place, redis caching in fastapi becomes a low-maintenance performance boost that scales from a laptop dev box to a multi-region production fleet. Happy caching!
Top comments (0)