"There are only two hard things in Computer Science: cache invalidation and naming things." That famous quote exists because caching is simultaneously the most impactful performance optimization and the trickiest to get right. Done well, caching can make your site 10-100× faster. Done poorly, it serves stale data to your users.
The Caching Layers
A typical web request passes through multiple caching layers:
- Browser Cache: Closest to the user. Stores assets locally on their device.
- CDN Cache: Distributed edge servers worldwide. Serves content from the nearest geographic location.
- Reverse Proxy Cache: Nginx, Varnish, or Cloudflare sitting in front of your application server.
- Application Cache: In-memory stores like Redis or Memcached for database query results, API responses, computed values.
- Database Cache: Query cache and buffer pool built into the database engine.
Browser Cache Headers
# Static assets (CSS, JS, images) — cache aggressively Cache-Control: public, max-age=31536000, immutable # HTML pages — don't cache, always revalidate Cache-Control: no-cache # API responses — cache for 5 minutes Cache-Control: private, max-age=300 # Sensitive data — never cache Cache-Control: no-store
Key Directives
public— Can be cached by browsers AND intermediate caches (CDNs, proxies)private— Only the user's browser can cache it (not CDNs)max-age=N— Cache for N secondsno-cache— Cache it, but always revalidate with the server before usingno-store— Don't cache at all. Use for sensitive data.immutable— The content will never change (used with fingerprinted filenames)
Cache Busting with Fingerprinted Filenames
The ideal caching strategy for static assets: give files a unique hash in the filename and cache them forever.
<!-- Instead of this (cache invalidation nightmare): -->
<link rel="stylesheet" href="/css/style.css">
<!-- Use this (infinitely cacheable): -->
<link rel="stylesheet" href="/css/style.a3f9b2c1.css">
<!-- When the CSS changes, the hash changes, so the browser
fetches the new file automatically -->
Build tools like Vite, Webpack, and esbuild generate these fingerprinted filenames automatically.
CDN Caching
A CDN (Content Delivery Network) caches your content on edge servers around the world. When a user in Tokyo requests your image, it's served from a server in Tokyo instead of your origin server in London.
- Static assets: Cache at the CDN edge with long
max-age+ fingerprinted filenames - HTML pages: Cache with short TTL (e.g., 60 seconds) or use
stale-while-revalidate - API responses: Cache GETs for public data, never cache POST/PUT/DELETE
Server-Side Caching with Redis
import Redis from 'ioredis';
const redis = new Redis();
async function getUser(userId) {
const cacheKey = `user:${userId}`;
// 1. Check cache first
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
// 2. Cache miss — query database
const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
// 3. Store in cache (expire after 5 minutes)
await redis.set(cacheKey, JSON.stringify(user), 'EX', 300);
return user;
}
// Invalidate on update
async function updateUser(userId, data) {
await db.query('UPDATE users SET ... WHERE id = $1', [userId]);
await redis.del(`user:${userId}`); // Clear cache
}
Cache Invalidation Strategies
- TTL (Time-To-Live): Cache expires after a fixed time. Simple but may serve stale data.
- Write-Through: Update the cache whenever you update the database. Consistent but adds write latency.
- Write-Behind: Update the cache immediately, flush to database asynchronously. Fast but risky.
- Cache-Aside: Application manages the cache manually (the Redis example above). Most common pattern.
- Event-Driven: Database changes emit events that invalidate relevant caches. Most robust for distributed systems.
Try Our Free Performance Tools
Minify your CSS, JavaScript, and HTML to reduce file sizes.