Skip to content

Caching Layers Explained: From Browser to Database

Aman Kumar Singh6 min read
Part 4 of 12From the System Design Fundamentals series
Caching Layers Explained: From Browser to Database — article by Aman Kumar Singh

Caching is the highest-leverage performance tool in most systems, and also the one that causes the most confusing bugs. Both facts come from the same source: a cache is a second copy of your data, and now you have to keep two copies from disagreeing. Before you reach for a specific caching pattern, it helps to have a map of where caches live, because the same request passes through several of them, and each one has different rules.

This is that map — the caching hierarchy from the browser all the way down to the database, and what belongs at each layer. Once you can see the whole stack, the concrete patterns like cache-aside and read-through and write-through have somewhere to attach.

The core idea, once

Every cache trades staleness for speed. You keep a copy of something expensive to produce somewhere fast to reach, and you accept that the copy might briefly disagree with the source of truth. Every decision in caching is a version of the same question: how much staleness can this particular piece of data tolerate, and how do I bound it? The answer differs enormously between a user's bank balance and the CSS for your homepage, which is why a single caching strategy for the whole system never works. You cache each thing according to how fresh it needs to be.

The layers, top to bottom

A request from a user's browser to your database passes through a surprising number of places that can answer it early. Here they are, in the order the request hits them.

Browser cache. The user's own browser stores static assets — images, scripts, stylesheets — according to the Cache-Control headers you send. This is the fastest cache of all, because the request never even leaves the user's machine. It costs you nothing to run and it is entirely controlled by the headers your server sets. The catch is invalidation: once a file is in a user's browser cache with a long lifetime, you cannot reach in and clear it. The standard fix is content-hashed filenames, so a changed file has a new URL and the old cached copy simply stops being requested.

CDN. A content delivery network is a fleet of servers spread around the world that cache your content close to users. When someone in Singapore requests an asset, they get it from a nearby CDN node instead of your origin server halfway across the planet. CDNs started with static assets but increasingly cache full API responses and rendered pages too. The wins are latency (physical distance shrinks) and origin offload (your servers never see the cached requests). This is the layer that makes a global audience feel local.

Application-level cache. Inside your own infrastructure, you cache computed results and frequently read data so your application does not recompute or re-fetch them. This is where a shared cache like Redis lives, holding query results, aggregates, session data, and anything expensive to produce and read more than it is written. This layer is the workhorse, and it is where most of your explicit caching logic and invalidation decisions happen. It is also where you must be careful about scoping keys per user or tenant, because a shared cache serving personalized data under a shared key is both a bug and a security leak.

Database cache. The database itself caches. Its buffer pool keeps frequently accessed pages in memory so reads do not hit disk, and it caches query plans so it does not re-plan the same query repeatedly. You mostly do not manage this directly, but it is worth knowing it exists, because it explains why the second run of a query is often far faster than the first, and why "it's slow only on a cold start" is a real phenomenon.

What to cache where

The layer you choose follows from what the data is.

  • Static assets (images, JS, CSS): browser and CDN. Give them long lifetimes and content-hashed names so you can cache aggressively without ever facing an invalidation problem.
  • Public, cacheable API responses (a product catalog, a public profile): CDN or application cache. These are read by many users and change rarely, which is the ideal caching profile.
  • Personalized or per-user data (a dashboard, a feed): application cache, scoped by user or tenant, with a short TTL. Cache it, but bound the staleness tightly and never under a shared key.
  • Expensive computed results (aggregations, reports): application cache. This is often the single highest-value thing to cache, because the cost of producing it is high and the result changes slowly.
  • Anything that must be perfectly current (a balance mid-transaction): do not cache, or cache with explicit invalidation you trust completely. Some data is not worth the risk.

TTLs and invalidation are the whole game

Once you know where to cache, the only remaining questions are how long a copy lives and how you get rid of it early. These two mechanisms — time-to-live and explicit invalidation — are what actually keep your copies from lying to users.

A TTL is your backstop. Even with perfect invalidation logic, every cached entry should have an expiry, because the one place you forget to invalidate is exactly where a stale value will haunt you. Add a little randomness (jitter) to your TTLs so that entries cached together do not all expire in the same instant, which is what causes a cache stampede that hammers your database the moment they lapse.

Explicit invalidation is what you use when data changes and you cannot wait for the TTL. When a user updates their profile, you clear or update the cached copy immediately rather than serving the old one until it expires. The hard part is knowing every place a piece of data is cached so your invalidation actually reaches all of them. Under-invalidate and you serve stale data; over-invalidate and you throw away cache value for no reason. Scoping your cache keys deliberately — the subject of caching best practices — is what keeps this manageable.

The failure mode nobody plans for

A cache is an optimization, not a source of truth, and your system has to survive it going away. If Redis becomes unavailable, requests should fall through to the database and the application should keep working, just slower. Code that treats a cache error as a hard failure turns a performance component into a single point of failure, which defeats the purpose entirely.

The subtle part is that when the cache disappears, the database suddenly absorbs all the load the cache was shielding it from. A system that runs comfortably with a warm cache can fall over the instant that cache empties, because the database was never sized for the full read volume. This is worth thinking through in advance: the question is not just "does the app survive a cache outage," but "does the database survive suddenly serving everything the cache used to."

Caching is where a lot of system design problems are won, because it is the cheapest way to make a read-heavy system fast. But every layer you add is another copy to keep honest. Add them deliberately, one at a time, each with a clear answer to how stale it is allowed to be.

Key takeaways

  • Every cache trades staleness for speed; the core question is always how much staleness a given piece of data can tolerate.
  • Requests pass through several caches — browser, CDN, application, database — each with different rules and lifetimes.
  • Cache static assets in the browser and CDN with content-hashed names; cache personalized data in the application layer, scoped per user, with short TTLs.
  • Every entry needs a TTL as a backstop, with jitter to avoid synchronized expiry and cache stampedes.
  • A cache is not a source of truth: a Redis outage should fall through to the database, and the database must be able to survive that sudden load.

Frequently asked questions

What are the different layers of caching?

From closest to the user inward: the browser cache (static assets on the user's machine), the CDN (content cached near users globally), the application cache (shared stores like Redis holding computed results and data), and the database's own buffer and query-plan caches.

What should I cache and where?

Static assets belong in the browser and CDN with long lifetimes and hashed filenames. Public, rarely-changing API responses fit the CDN or application cache. Personalized per-user data belongs in the application cache, scoped per user with a short TTL. Data that must be perfectly current should not be cached, or only with invalidation you fully trust.

Why does every cache entry need a TTL?

A TTL bounds staleness and rescues you when you forget to invalidate somewhere. Even with explicit invalidation, the one place you miss is exactly where a stale value will surface. Add jitter so entries cached together do not all expire at once, which triggers a cache stampede on your database.

What happens to my system if the cache goes down?

With proper error handling, requests fall through to the database and the app keeps working, just slower. The risk is that the database then absorbs all the load the cache was shielding it from, so a system that runs fine with a warm cache can fall over when the cache empties unless the database is sized for it.

Related articles

Rate Limiting Algorithms: Fixed Window, Token Bucket, and More — Aman Kumar Singh
Consistent Hashing Explained: Distributing Data Without the Reshuffle — Aman Kumar Singh
Horizontal vs Vertical Scaling: When to Scale Out, When to Scale Up — Aman Kumar Singh

Explore more on

Aman Kumar Singh

About the author

I'm Aman Kumar Singh, a software engineer in Noida, India building scalable full-stack products with React, Next.js, Node.js, NestJS, PostgreSQL, Redis, and AWS. I write about backend engineering, distributed systems, and system design.