There is a famous quote in computer science: "There are only two hard things in computer science: cache invalidation and naming things." Cache invalidation is hard because serving stale data is often worse than serving no cache at all.
Here is a layered caching approach that balances performance and correctness.
Layer 1: Browser Cache (HTTP Headers)
For static assets (JavaScript bundles, CSS, images), aggressive caching is almost always correct. If you use content-hashed filenames (which Vite and most modern bundlers do by default), the filename changes when the content changes, so you can cache forever:
Cache-Control: public, max-age=31536000, immutableFor HTML files and API responses, be much more conservative. HTML files should not be cached aggressively because they reference your hashed assets:
Cache-Control: no-cacheNo-cache does not mean "do not cache" — it means "check with the server before using the cache." The browser still stores the content; it just revalidates with an ETag on each request.
Layer 2: CDN Cache
A CDN sits between your users and your origin server and caches responses closer to users geographically. Configure your CDN to cache static assets aggressively and API responses selectively.
For API endpoints that return public, slowly-changing data (like a sitemap, a homepage config, or public resource listings), short CDN caches (5-60 seconds) dramatically reduce load on your origin with minimal staleness risk.
Layer 3: Application Cache (Redis)
For database-heavy computations that are called frequently, Redis is the standard application-level cache. The pattern is always:
async def get_hall_of_fame():
cached = await redis.get("hall_of_fame")
if cached:
return json.loads(cached)
data = await db.fetch("SELECT ... LIMIT 6")
await redis.setex("hall_of_fame", 300, json.dumps(data)) # 5 min TTL
return dataInvalidation Strategy
The hardest part: when should cached data be cleared?
- Time-based TTL: Simple and safe. Cache expires after N seconds regardless of changes.
- Event-based invalidation: Clear the cache when the underlying data changes. More complex but serves fresher data.
- Cache-aside with versioning: Append a version key to cache entries. When you need to invalidate everything, increment the version.
Choose time-based TTL as your default. Add event-based invalidation only where staleness causes real user pain.