Caching Patterns
Cache-aside, read-through, write-through and write-behind differ in who talks to whom and therefore in who is responsible for keeping the cache honest — and every one of them has a window where the cache is wrong.
Why cache at all
The same small fraction of data is read far more than it changes: the popular products, the logged-in user’s own profile, the rendered home page. Serving those from memory removes most of the database’s read load for a fraction of the cost of another replica, and cuts read latency from milliseconds to microseconds. The catch is that a cache is a second copy of the truth, and a second copy can disagree with the first.
The four patterns
Cache-aside (the default): the application checks the cache, and on a miss reads the database and populates the cache itself. On a write, it updates the database and deletes the cache key. Simple, the cache can fail without taking the app down, and only what is read gets cached. The burden: every write path must remember to invalidate, and a forgotten delete is a bug that looks like random staleness.
Read-through: the cache itself loads from the database on a miss, so the application only ever talks to the cache. Centralises the load logic, at the cost of the cache being in the critical path of every read. Write-through: writes go through the cache, which writes the database synchronously — read-after-write consistency, slower writes, and everything written is cached whether read or not. Write-behind: writes land in the cache and flush to the database in batches later — very fast writes, coalesced updates, and a durability risk if the cache dies before the flush.
TTL, eviction, and the metric that matters
A TTL bounds staleness: set it to the longest a value may safely be wrong. Eviction handles a full cache: allkeys-lru for a pure cache, and never the default noeviction, which turns a full cache into write failures. The number to watch is hit rate — hits over hits plus misses. Below ~80% the cache is barely helping and every miss still hits the database; above ~95% the database sees a fraction of the traffic. Hit rate is driven by TTL, by working-set size versus cache size, and by how skewed the access is.
Key points
- Cache the reads that are frequent and tolerate being slightly old.
- Cache-aside is the default: app populates on miss, deletes on write. Prefer delete over update on write.
- Read-through centralises loads; write-through gives read-after-write; write-behind trades durability for write speed.
- Set TTL to tolerable staleness, choose an eviction policy deliberately, watch the hit rate.
Four caching patterns
The application talks to both.
1. app: GET cache[user:7] 2. cache: miss 3. app: SELECT … FROM users WHERE id = 7 4. app: SET cache[user:7] = row, TTL 300s 5. app: return row
When to use — and when not
- A read far more frequent than the writes that would invalidate it, that tolerates bounded staleness.
- Data that must always be current with no bypass.
- Writes as the bottleneck — a cache does not scale writes.
- When a well-indexed query is already fast enough.
Failure modes
- Forgotten invalidation on one write path.
- noeviction on a full cache.
- Caching data that changes as often as it is read, so the hit rate is low and staleness high.
See how this works internally →
Descend one layer: the same topic explained from the machinery up.