The Fastest Request is the One You Never Make
Caching is the most powerful performance optimization technique in computing. At its core, caching is simple: store the result of an expensive operation so you don't have to repeat it. But in practice, caching is nuanced — choosing what to cache, where to cache it, when to invalidate it, and how to handle stale data are decisions that can make or break your application's performance.
A well-designed caching strategy can reduce database load by 90%, cut response times from seconds to milliseconds, and dramatically reduce infrastructure costs. A poorly designed one can serve stale data, cause consistency bugs, and create debugging nightmares.
The Caching Layers
Modern web applications benefit from caching at multiple layers. Each layer has different characteristics, trade-offs, and use cases.
| Layer | Technology | Latency | Best For |
|---|---|---|---|
| Browser Cache | HTTP headers | ~0ms | Static assets, API responses |
| CDN | CloudFront, Cloudflare | ~5-30ms | Static files, edge-cached pages |
| Application Cache | Redis, Memcached | ~1-5ms | Session data, computed results |
| Database Cache | Query cache, materialized views | ~5-20ms | Frequent queries, aggregations |
| CPU Cache | L1/L2/L3 | ~1-10ns | Hot code paths (handled by hardware) |
Common Caching Patterns
Cache-Aside (Lazy Loading)
The most common pattern. The application checks the cache first. On a cache miss, it loads data from the database, stores it in the cache, and returns the result. This pattern is simple, resilient (the app works without the cache), and only caches data that's actually requested.
async function getUser(id: string): Promise<User> {
// 1. Check cache
const cached = await redis.get(`user:${id}`);
if (cached) return JSON.parse(cached);
// 2. Cache miss — load from DB
const user = await db.query.users.findFirst({
where: eq(users.id, id)
});
if (!user) throw new Error('User not found');
// 3. Store in cache with TTL
await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 3600);
return user;
}Write-Through
In the write-through pattern, data is written to both the cache and the database simultaneously. This ensures the cache is always up-to-date, eliminating the cache-miss penalty for recently written data. The trade-off is increased write latency.
Write-Behind (Write-Back)
Write-behind is the high-performance variant: data is written to the cache first, and asynchronously flushed to the database in batches. This provides excellent write performance but introduces a risk window where data exists only in the cache.
Choosing the Right Pattern
Cache-aside works for 90% of use cases. Use write-through when you need strong consistency between cache and database. Use write-behind when write performance is critical and brief data loss is acceptable.