A cache trades a small amount of storage — usually fast, expensive, in-memory storage — for a large reduction in latency and load on whatever's behind it (typically a database). The strategy questions are: when does data get written into the cache, and when does it get thrown out?
| Strategy | How it works | Trade-off |
|---|---|---|
| Cache-aside (lazy loading) | App checks cache first; on a miss, reads from the DB and writes the result into the cache itself | Only requested data ever gets cached, but the first request for any given key always eats a cache miss |
| Write-through | Every write goes to the cache and the DB, synchronously, as one operation | Cache is never stale, but every write pays the latency cost of both writes |
| Write-back (write-behind) | Write goes to the cache immediately; the DB write happens later, asynchronously | Writes are fast, but data sitting only in cache is lost if the cache crashes before it's flushed |
Cache-aside is by far the most common pattern in practice, precisely because it only spends cache space on data that's actually being requested — a key nobody asks for never occupies a slot. The code shape is always the same:
function get(key):
value = cache.get(key)
if value is not None:
return value # cache hit
value = database.get(key) # cache miss — go to the source of truth
cache.set(key, value)
return value
Write-through guarantees the cache is never out of sync with the database, because every write updates both before it's considered done — the cost is that every write now waits on two systems instead of one. Write-back flips that trade: writes return fast because only the cache has to acknowledge them, but there's now a window where the only copy of the freshest data lives in a volatile cache, and a crash in that window loses writes the client already believes succeeded.
A cache can't grow forever, so entries need a way to leave. Two independent mechanisms usually work together: a TTL (time-to-live), after which an entry is considered stale and is dropped or refreshed regardless of how popular it is, and an eviction policy, which decides what to remove specifically when the cache is full and something new needs room.
| Policy | Evicts | Good fit | Weak point |
|---|---|---|---|
| LRU (Least Recently Used) | The entry that hasn't been accessed in the longest time | General-purpose — recent access is usually a decent predictor of near-future access | A single burst of one-off scans can evict genuinely hot data it hasn't touched in a while |
| LFU (Least Frequently Used) | The entry with the lowest total access count | Data with a stable "hot set" that gets accessed far more often than everything else | A once-popular item can stay stuck in the cache long after it's stopped being requested, because its historical count is high |
Neither one is universally "better" — LRU adapts faster to a shifting access pattern, LFU is more resistant to a temporary spike of one-off accesses evicting your genuinely hot data. Most production caches (Redis included) default to an LRU-family policy specifically because "recently accessed" tends to be a good enough proxy for "likely to be accessed again soon" across a wide range of real workloads.
A Content Delivery Network is a geographically distributed layer of caching servers (edge servers / points of presence) that sit physically closer to end users than your origin servers do, and cache content — images, video, JS/CSS bundles, and anything else that doesn't change per-request — at those edge locations. A user in Mumbai requesting a static image ideally never makes a round trip to an origin server in Virginia at all; the nearest edge node already has it cached and serves it directly, which cuts both latency (shorter physical distance) and load on the origin (the edge absorbs the repeat requests entirely).
CDNs are a great fit for content that's identical for every user and changes rarely — static assets are the canonical example — and a poor fit for anything genuinely personalized or that must reflect the very latest write, for the same reason any cache is a poor fit for that kind of data: a cache's entire value proposition rests on serving the same answer to many requests instead of computing it fresh each time.
There's an old joke in computer science that there are only two hard problems: cache invalidation and naming things. The reason invalidation earns that reputation is that a cache is, by definition, a second copy of the truth, and the moment the original changes, that copy is stale until something notices and fixes it — and "something notices" is a genuinely hard problem to get right at scale.
The three common approaches, in roughly increasing order of correctness and decreasing order of simplicity:
Common mistake: Reaching for a longer TTL to "fix" a stale-cache bug instead of fixing the actual invalidation path. A longer TTL doesn't make data more correct — it just makes the staleness window last longer between the same underlying gaps in invalidation logic.