HomeProjectsBlogContact

Caching Strategies for Web Apps: Browser, CDN, Server and Database

Caching is the cheapest performance win there is, and the easiest way to serve stale data. How each cache layer works, which headers to use, and how to invalidate without tears.

Caching Strategies for Web Apps: Browser, CDN, Server and Database

There's an old joke that the two hardest problems in computer science are cache invalidation, naming things and off-by-one errors. It's funny because caching really is both the cheapest performance win available and a reliable source of "why am I seeing old data?" bugs.

The trick is knowing which layer to cache at and how each one expires.

The layers, from closest to the user outward

  1. Browser cache: files stored on the user's device.
  2. CDN / edge cache: copies stored on servers near the user.
  3. Application cache: computed results in memory or Redis.
  4. Database: its own internal caches, plus whatever you precompute.

A request that's answered by layer 1 never touches the network. One answered by layer 2 never reaches your server. Every layer you skip saves time and money.

Browser caching with Cache-Control

The Cache-Control header tells browsers (and CDNs) what they may store and for how long.

Static assets with hashed filenames (app.3f9a1c.js, logo.8b2e.png): cache them forever. When the content changes, the filename changes, so there's nothing to invalidate.

Cache-Control: public, max-age=31536000, immutable

Most modern build tools hash filenames automatically. This one header is often the biggest repeat-visit speedup you can get.

HTML pages: these reference the hashed assets, so they must stay fresh.

Cache-Control: no-cache

no-cache doesn't mean "don't store." It means "check with the server before using it." Combined with an ETag, the server can answer 304 Not Modified with no body, which is fast and cheap.

Private, user-specific responses (account pages, API responses with personal data):

Cache-Control: private, no-store

private keeps shared caches like CDNs from storing them. no-store prevents storage entirely, appropriate for sensitive data.

CDN caching

A CDN stores responses at edge locations around the world. For static sites it's automatic. For dynamic pages and APIs, you control it with headers:

Cache-Control: public, s-maxage=300, stale-while-revalidate=600
  • s-maxage=300: shared caches (the CDN) may serve this for 5 minutes.
  • stale-while-revalidate=600: after that, serve the stale copy instantly while fetching a fresh one in the background.

That combination is wonderful for blogs, product pages and public listings: users always get a fast response, and content is never more than a few minutes old.

Frameworks wrap this in friendlier APIs. Next.js revalidation, for example, is built on the same idea of serving cached content and refreshing it periodically or on demand.

Watch out for: caching responses that vary by cookie, language or device without telling the CDN. Use the Vary header or keep personalised responses out of shared caches.

Application caching (cache-aside)

For expensive computations or slow queries, cache results in your app, usually in Redis or in memory.

The most common pattern is cache-aside:

async function getProduct(id) {
  const key = `product:${id}`
  const cached = await redis.get(key)
  if (cached) return JSON.parse(cached)

  const product = await db.products.findById(id)
  await redis.set(key, JSON.stringify(product), { EX: 600 }) // 10 minutes
  return product
}

Check the cache; on a miss, load from the database and store the result with a TTL.

In-memory vs Redis: an in-process cache (a Map or an LRU library) is fastest but isn't shared between server instances and disappears on restart. Serverless functions make in-memory caching unreliable. Redis (including hosted options like Upstash) is shared and survives deploys.

Invalidation: the hard part

Three strategies, from simplest to most precise:

  1. Time-based (TTL). Data expires after N seconds. Simple and robust. Pick a TTL that matches how stale the data is allowed to be.
  2. Delete on write. When you update a product, delete product:{id} from the cache. The next read repopulates it.
  3. Versioned keys. Include a version or timestamp in the key (product:42:v7). Updating changes the version, and old entries just expire.

Combining TTL with delete-on-write covers most applications: fresh after edits, and self-healing if a deletion is ever missed.

Cache stampedes

When a popular cache entry expires, hundreds of requests can miss at once and all hit the database simultaneously. Defences include:

  • Stale-while-revalidate at the application level: serve the old value while one request refreshes it.
  • Locking: only one request recomputes; others wait briefly or get the stale value.
  • Jitter: add randomness to TTLs so many keys don't expire at the same moment.

What not to cache

  • Anything where stale data causes real harm: account balances, stock levels at checkout, permissions.
  • Personal data in shared caches.
  • Error responses, unless deliberately and briefly. Caching a 500 for an hour is a bad afternoon.

Measure it

Track your cache hit ratio. A cache with a 20% hit rate is mostly overhead. Check response headers too: most CDNs add a header that shows whether a response was a HIT or a MISS.

Caching works best on top of a fast baseline. If the underlying query is slow, also look at indexing and the N+1 problem. And for the full performance picture: why is my website slow?

FAQ

What is the best caching strategy for a web application?

Cache hashed static assets forever, keep HTML revalidating, use CDN caching with stale-while-revalidate for public pages, and use cache-aside in Redis for expensive queries, with TTLs plus delete-on-write.

What's the difference between no-cache and no-store?

no-cache allows storing but requires revalidation with the server before each use. no-store forbids storing the response at all.

Why is my site showing old content after deploying?

Usually the HTML is cached too aggressively, or assets don't have hashed filenames. Set no-cache on HTML, hash asset filenames, and purge your CDN cache after deploys if needed.

Keep reading

More from the workbench

All articles →