01 — Backend Performance: Caching (Layers, Patterns, Honesty)
Only 3 items, yet together they reduce most backend latency in the wild [1] — the roadmap opens with caching for a reason. My shorthand for the whole section: cache at every layer you can, pick the right write pattern per use case, then pay attention to invalidation before stale data bites you. Three items, one per move.
The layers a request passes through, each of which can cache:
The first roadmap item is the picture: utilize caching mechanisms (HTTP, server/client, CDN) — meaning don't pick one layer; use as many layers as the workload tolerates. Browser cache with proper Cache-Control headers, CDN edge cache for static and ISR pages (server-rendered pages cached as static HTML), application in-process cache for hot reads inside one process, and a distributed cache (Redis, Memcached) shared across process replicas. Each tier you stack in front of the database removes a class of requests from ever reaching it. The rule of thumb: cache it earlier when you can (browser, CDN) because earlier layers are cheaper to reach, and use later layers (app memory, Redis) for what earlier layers can't hold — per-user data, computed results, lookups.
The catch with stacking layers — and the reason 3 items rather than 1 — is that each layer is a place the data can be wrong. That's where the second and third items live.
Pick the right write pattern
The second item: use cache-aside, write-through, or read-through caching patterns based on your application requirements. Three patterns, three different correctness/performance tradeoffs:
- Cache-aside (lazy-loading): the app reads from the cache, and on a miss it reads from the DB, writes the result to the cache, and returns. The cache is a side table, not on the write path. Lazy, simple, tolerates cache loss without data loss; the cost is a thundering herd on a cold cache after a flush. Right for read-heavy, change-rare workloads like product catalogs.
- Read-through: the app reads only from the cache; on a miss the cache itself fetches from the DB and populates itself. Simpler application code (one read API), but the cache becomes a hard dependency — lose it and your reads degrade. Right when you control the cache library and want one abstraction.
- Write-through: the app always writes to the cache and the cache synchronously writes through to the DB. Reads are always consistent with the latest write — never stale — at the price of slower writes (one extra hop). Right when consistency matters more than write throughput: account balances, configuration, anything where stale-on-read is a bug.
The model that helped me: cache-aside treats the cache as an optimization the app owns; read-through treats the cache as the read API; write-through treats the cache as the source of truth for reads. The choice is "what does my app do when the cache disagrees with the DB," and the answer has to be deliberate.
Keep cached data honest
The third item: use proper cache-invalidation strategies to ensure data consistency and prevent stale content. This is the part nobody budgets for, and it's where most caching bugs live. The strategies in increasing strictness:
- TTL (EXPIRE) — the simplest and the most common. Every cached value has an expiry; for the duration of the TTL the cache lies to you about freshness, and after it expires the lie ends. Right when the data changes slowly and "a few minutes stale is fine" — a product description, a leaderboard. Wrong when the data is a balance or a permission flag.
- Event-driven invalidation — the DB write publishes an event (a cache-busting message, a DEL against the key, a revalidateTag call). Price: you now own a publish path on every write, and you have to remember to publish from every write path. Right when stale-on-read is unacceptable and the write paths are few and known.
- Versioned keys — never overwrite user:123; write user:123:v5 and serve the new key. Old keys age out by TTL. Right when the value can change shape and you want atomic swap without races; common in HTTP asset caching (hashed filenames) and rare in app-level caching.
Two failure modes I keep biting me: thundering herd on a popular expired key (every request misses at once, every request refetches) — fix with a lock around the refetch, or with stale-while-revalidate — serve the stale value immediately while a background refresh repopulates the cache. And stale across the cache clear — when you flush the cache and the app refills it from data that's about to be updated again, you serve yesterday's data indefinitely until the TTL rotates. Fix with shorter TTLs or event-driven invalidation on the high-value keys.
The three items in practice
When I'm auditing a backend I walk the layers in the diagram's order — does the browser cache anything? Does the CDN? Is there a per-process LRU? A shared Redis? — and then I ask the invalidation question for each: what's the TTL? What publishes when the underlying data changes? Where would stale data show up? The shape of most caching bugs is exactly one of those two questions answered lazily. Three items, one diagram, but it's the half of backend performance most teams underdo well — not because caching is hard, but because the third item (keep the data honest) is invisible until production disagrees with you.
References
- [1] roadmap.sh, "Backend Performance Best Practices — Caching," roadmap.sh, 2024. [Online]. Available: https://roadmap.sh/backend-performance-best-practices
- [2] Redis, "Client-side caching," Redis Docs, 2024. [Online]. Available: https://redis.io/docs/latest/develop/use/client-side-caching/
- [3] AWS, "Caching Strategies," Amazon Builders' Library, 2024. [Online]. Available: https://docs.aws.amazon.com/whitepapers/latest/database-caching-strategies-using-redis/caching-patterns.html
- [4] MDN, "Cache-Control," Mozilla, 2024. [Online]. Available: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
Comments
Leave a Comment
You must be signed in to comment
0 Comments
No comments yet. Be the first to comment!