Skip to content

Databases

Caching Strategies

Caching sits between a client and something expensive it's asking for — a database query, an API call, a rendered page — and returns a stored copy instead of redoing the work, as long as that copy is still considered valid. Most of the discipline comes down to two questions: where to put the cached copy (browser, CDN, application memory, a dedicated store like Redis), and when to decide it's stale and throw it away.

Why it matters

It's often the highest-leverage performance fix available
Avoiding a slow query entirely is usually a bigger win than trying to make that same query marginally faster.
It reduces load on the systems behind it
A cache absorbs repeated requests for the same data, which keeps a database or a rate-limited upstream API from taking the full weight of every single request.
Deciding when data is stale is a genuinely hard problem
It's referenced often enough as one of the two hard problems in computer science, precisely because getting invalidation wrong produces bugs that are easy to introduce and hard to notice.

Cache-aside, the default pattern

On a read, check the cache first; if the value isn't there (a cache miss), fetch it from the real source, store it in the cache, and return it. Later reads for the same key hit the cache directly until the entry expires or is explicitly invalidated. It's the simplest pattern to reason about and layer onto an existing system incrementally.

JavaScript
async function getUser(id) {
  const cached = await cache.get("user:" + id);
  if (cached) return JSON.parse(cached);

  const user = await db.users.findById(id);
  await cache.set("user:" + id, JSON.stringify(user), "EX", 300);
  return user;
}

Invalidation and expiry

A time-to-live (TTL) is the simplest default: a cached value just expires after a fixed period, trading some staleness for simplicity. Explicit invalidation — clearing a key the moment the underlying data actually changes — is more precise but adds real complexity, since every code path that writes that data has to remember to clear the right cache key. A 'cache stampede' happens when a popular key expires and many concurrent requests all miss at once and try to recompute the same expensive value simultaneously.

Where to cache: browser, CDN, app, store

Caching exists at every layer of a request: the browser can cache a response using standard HTTP headers, a CDN can cache it closer to the user, the application can hold a value in memory or in a store like Redis, and the database itself caches query results internally. Picking the right layer matters — an elaborate application-level cache is often unnecessary work when an HTTP cache header would have solved the same problem with far less code.

Mistakes people make here

Caching per-user or per-request data under a shared key
Reusing one cache key across different users' requests can leak one user's data to another the moment a second request hits the cached value meant for someone else entirely.
Setting no expiry at all
A cached value with no TTL lives forever until someone remembers to clear it manually, which reliably doesn't happen — stale data quietly accumulates until it's discovered as a bug.
Not planning for a cache stampede
When a hot key expires, every concurrent request that misses can try to recompute the same expensive value at the same moment, which can hit the underlying database or API with a sudden spike it wasn't sized for.
Caching at the wrong layer
Building an elaborate application-level cache for something a CDN or a standard HTTP Cache-Control header would already solve adds code and a new source of bugs for no real benefit over the simpler option.

Strengths and trade-offs

Where it is strong

  • Often the single highest-leverage performance fix available, since it avoids redoing expensive work rather than trying to make that work itself faster.
  • Layers exist at every level — browser, CDN, application, database — so caching can happen close to the request instead of always reaching the origin.
  • Patterns like cache-aside are simple to reason about and can be added incrementally onto an existing system.

The trade-offs

  • Invalidation is genuinely hard — stale data served from a cache is a real, recurring bug class, not a theoretical concern.
  • A cache is a second source of truth that can drift from the underlying data if the invalidation logic has any gap.
  • It adds infrastructure — a cache store, or at minimum more code — that has to be operated and monitored on its own.
  • Over-aggressive caching can hide real performance or correctness problems until the cache is cleared and everything hits the origin at once.

Who needs this

Relevant to anyone building an application that reads the same data repeatedly, or that calls a slow or rate-limited upstream service.

Questions about caching strategies

What's the difference between cache-aside and write-through?
Cache-aside loads a value into the cache only on a read miss; write-through updates the cache at the same time as the underlying write, so a miss is rarer, at the cost of every write doing more work up front.
How long should a TTL be?
There's no universal number — it depends entirely on how stale that specific data is allowed to be before it matters, which varies from seconds to hours or more depending on what's being cached.
What is a cache stampede?
It's when many concurrent requests all recompute the same expired, popular key at the same moment; it's typically addressed with locking around the recomputation, refreshing a key early before it expires, or staggering expiry times.
Is caching only relevant to databases?
No — DNS lookups, CDN responses, browser assets, API responses, and expensive computed values are all cached using the same underlying ideas of storing a copy and deciding when it's stale.

The primary source

Related concepts

← All concept guides