What Is Caching? Cache Strategies, Explained
Caching keeps a copy of expensive data somewhere faster. How cache-aside, write-through, and TTLs work — and why invalidation is the hard part.
Caching is keeping a copy of data that is expensive to compute or fetch in a place that is cheaper and faster to read. That is the entire idea. A database query that takes 80 milliseconds gets stored in memory where it takes 0.5; a page generated on a server in Virginia gets copied to an edge node in Tokyo. Nearly every performance win in web architecture is some form of caching, and nearly every maddening “why am I seeing old data” bug is its cost. The engineering skill is not turning caching on — it is deciding what to cache, where, for how long, and how the copy gets refreshed when the truth changes.
One request, four caches
Follow a single page load and you will usually cross at least four cache layers:
- The browser cache. The user’s own machine keeps copies of assets (and sometimes whole responses) so repeat visits skip the network entirely. This is the fastest cache there is — zero milliseconds beats any server.
- The CDN. A content delivery network holds copies of responses at edge locations near users, absorbing traffic before it ever reaches your origin.
- The application cache. An in-memory store like Redis or Memcached sits beside your servers holding query results, rendered fragments, and session data.
- The database buffer pool. Even your database caches: PostgreSQL keeps recently read pages in RAM, which is why a repeated query is fast even with no cache in front of it.
These four are the types of caching you will meet in web work, and the principle does not stop there: inside your own code, memoization caches a function’s return value, and below everything the CPU’s L1/L2/L3 caches play the same trick with RAM itself.
Each layer absorbs requests so the layer below sees fewer of them. When people say “add caching,” they usually mean layer 3 — the application cache — and that is where strategy choices matter most.
The core strategies
Cache-aside (lazy loading). The application checks the cache first; on a miss it reads the database, then writes the result into the cache for next time. This is the default pattern in most codebases. Its strengths are simplicity and efficiency — only data that is actually requested ever occupies memory. Its weakness is that every entry is stale from the moment the underlying data changes until the entry expires or is explicitly invalidated.
Read-through. Same read path as cache-aside, but the cache itself is responsible for loading from the source on a miss — the application only ever talks to the cache. This centralizes loading logic and pairs well with libraries and managed caching layers that support it.
Write-through. Every write goes to the cache and the database synchronously, so the cache is always warm and consistent with the store. The price is write latency — each write does double duty — and memory spent caching data that may never be read.
Write-behind (write-back). Writes land in the cache and are flushed to the database asynchronously. Writes become extremely fast and bursts get smoothed out, but now the cache briefly holds the only copy of new data — a crash before the flush loses writes. Use it only where that risk is acceptable or mitigated.
TTL expiry. Not a strategy so much as the universal backstop: every entry carries a time-to-live after which it is discarded. A short TTL bounds how stale data can get even when invalidation logic misses a case. Almost every production cache combines one of the strategies above with TTLs.
| Strategy | Reads | Writes | Staleness risk | Best for |
|---|---|---|---|---|
| Cache-aside | App checks cache, falls back to DB | App writes DB only | Until TTL or invalidation | General-purpose, read-heavy |
| Read-through | Cache loads on miss | App writes DB only | Until TTL or invalidation | Centralizing cache logic |
| Write-through | Always warm | Synchronous dual write | Low | Consistency-sensitive reads |
| Write-behind | Always warm | Async flush to DB | Low for reads, loss risk on crash | Write-heavy bursts |
Why invalidation is genuinely hard
The old joke — there are only two hard things in computer science, cache invalidation and naming things — survives because it is true. Invalidation is hard for a structural reason: the cached copy has no idea the underlying data changed. Something has to notice the change and evict or update the copy, and that something is application code spread across every path that mutates the data. Miss one path — a bulk import, an admin tool, a second service writing to the same table — and you serve stale data with no error anywhere.
Distribution makes it worse. Invalidate an entry at the same moment another server repopulates it from a read it started before your write, and the stale value gets written back after the invalidation — now it is pinned until the TTL saves you. This race is why short TTLs are a safety net rather than an admission of defeat, and why teams with strict freshness requirements version their keys (user:42:v7) instead of deleting them in place.
Stampedes and thundering herds
A popular cache entry expires, and in the next hundred milliseconds ten thousand requests all miss simultaneously — and all ten thousand hit the database with the same expensive query. That is a cache stampede, and it can take down an origin that runs comfortably at a 99% hit ratio. The standard mitigations:
- Request coalescing. Let one request rebuild the entry while the rest wait for it (a short lock or “single flight” pattern), instead of all rebuilding at once.
- Jittered TTLs. Add randomness to expiry times so a batch of entries cached together does not expire together.
- Stale-while-revalidate. Keep serving the expired value while one background refresh fetches the new one. Users see slightly old data for a moment; the database sees one query instead of thousands.
HTTP caching in one minute
Layers 1 and 2 are driven by response headers — Cache-Control and ETag do most of the work. Cache-Control: max-age=3600 tells the browser how long a copy stays fresh; s-maxage does the same for shared caches like CDNs; ETag lets a client revalidate cheaply — the server replies 304 Not Modified and no body crosses the wire. Getting these right on static assets is one of the highest-leverage moves in the Core Web Vitals playbook: assets with long max-age and hashed filenames never need to be fetched twice.
The metric that matters
A cache is judged by its hit ratio — the fraction of lookups served from the cache. The number to internalize: going from a 90% to a 99% hit ratio does not improve origin load by 9%, it cuts it by 10×, because misses fall from 10% of traffic to 1%. So measure hits and misses per key family, size memory to your working set, and watch eviction counts — most stores evict the least recently used entries when memory fills, and a cache that is constantly evicting entries before they are re-read is just a slower way to query the database. Which store to put behind all this is its own question — see Redis vs Memcached for that comparison.
The takeaway
Caching is the art of serving yesterday’s answer fast while making sure “yesterday” is recent enough not to matter. Cache-aside with sensible TTLs is the right default; write-through buys consistency at write cost; write-behind buys write speed at durability cost. Expect invalidation to be the hard part, defend hot keys against stampedes, and measure hit ratio before and after every change. The fastest query is the one that never reaches the database.
Frequently asked questions
- What are the types of caching?
- A typical web request crosses four types of caching: the browser cache on the user's device, CDN edge caches, an application cache such as Redis or Memcached, and the database's own buffer pool. Inside code, memoization and CPU caches apply the same idea.
- What is cache invalidation?
- Cache invalidation is removing or updating a cached copy when the underlying data changes, so readers stop seeing stale values. It is famously hard because every code path that mutates the data must also invalidate it, and distributed races can re-pin stale entries.
- What is a good cache hit ratio?
- Read-heavy production caches typically aim for 90% or higher. Improvements compound at the top: going from a 90% to a 99% hit ratio cuts origin load by 10x, because misses fall from 10% of traffic to 1%.
Keep reading
Takina · · 4 min read What Is the Beacon API? navigator.sendBeacon()
The Beacon API lets a page send one last async request as it unloads, without blocking navigation or racing the browser's page teardown.
The Lycoris Team · · 4 min read What Is Write Amplification? SSDs and Databases
Write amplification is when a system writes more data physically than the logical write requested, wearing out storage faster and hurting throughput.
The Lycoris Team · · 4 min read Redis Persistence: RDB vs AOF, Explained
Redis is in-memory, so RDB snapshots and the AOF log are how it survives a restart — each trades durability against performance differently.