Back to Blog
Performance11 min read2026-07-09

Caching Strategies for Web Applications

Caching is the cheapest way to make an app faster and cheaper to run, until invalidation bites you. A practical tour of cache layers, patterns, eviction, and avoiding stampedes.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

# Caching Strategies for Web Applications

There are only two hard problems in computer science, the joke goes, and cache invalidation is one of them. Caching is also the single cheapest way to make an application dramatically faster and cheaper to run. The trick is knowing *what* to cache, *where*, and *how to invalidate it* without serving stale data. Let's build a mental map.

The cache layers

A web request can hit caches at many points. Each layer catches what the layers behind it would otherwise have to compute:

Browser cache → CDN → Reverse proxy → App-level cache → Database cache
   (closest to user, cheapest)        →        (farthest, most expensive)

The rule of thumb: cache as close to the user as you safely can. A request served from a CDN edge never touches your origin at all.

LayerCachesTypical TTLInvalidation
BrowserStatic assets, API responsesminutes–yearCache-Control, versioned URLs
CDNStatic + cacheable dynamicseconds–daysPurge API, cache keys
App (Redis/Memcached)Query results, sessions, fragmentsseconds–hoursExplicit delete, TTL
DatabaseQuery/buffer cacheautomaticManaged by DB

HTTP caching: get the headers right

Much caching is free if you set HTTP headers correctly. Key ones:

Cache-Control: public, max-age=31536000, immutable   # fingerprinted assets
Cache-Control: private, no-cache                       # per-user, revalidate
ETag: "a1b2c3"                                          # validator for revalidation
  • Fingerprinted assets (app.a1b2c3.js) can be cached forever with immutable because a new build produces a new filename.
  • HTML and dynamic responses usually want short TTLs or revalidation via ETag/Last-Modified.
  • stale-while-revalidate lets a CDN serve slightly stale content instantly while fetching a fresh copy in the background — great for perceived performance.

Application caching patterns

For data your app computes, several patterns exist:

Cache-aside (lazy loading)

The most common. The app checks the cache; on a miss, it loads from the DB and populates the cache.

async function getUser(id) {
 const cached = await redis.get(`user:${id}`);
 if (cached) return JSON.parse(cached);

 const user = await db.users.findById(id);
 await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 300); // 5 min TTL
 return user;
}

Simple and resilient (cache outage just means more DB load), but the first request is always a miss, and you must invalidate on writes.

Write-through

Writes go to the cache and the database together, keeping them in sync. Reads are always warm, but writes are slightly slower and you cache data that may never be read.

Write-behind (write-back)

Writes hit the cache and are flushed to the DB asynchronously. Fast writes, but risk of data loss if the cache fails before flushing. Use carefully.

Invalidation: the hard part

Three broad approaches, often combined:

  1. 1TTL expiry. Simplest — data is stale for at most N seconds. Choose TTLs by how much staleness the feature tolerates. A leaderboard can be 30s stale; a bank balance cannot.
  2. 2Explicit invalidation. On write, delete or update the relevant keys. Precise but you must remember every dependency — easy to miss one and serve stale data.
  3. 3Versioned/keyed caching. Bake a version into the key (user:42:v3). Bump the version to invalidate without deleting. Elegant for cache busting.

A practical hybrid: short TTLs as a safety net *plus* explicit invalidation on writes for freshness. The TTL bounds your worst case even when you forget an invalidation.

Eviction policies

Caches are finite, so when full they evict. Know your store's policy:

  • LRU (least recently used) — the common default, good for most workloads.
  • LFU (least frequently used) — favors hot keys.
  • TTL-based — evicts on expiry regardless of use.

In Redis, set maxmemory and a policy like allkeys-lru so the cache degrades gracefully instead of erroring when full.

Beware the cache stampede

When a popular key expires, hundreds of concurrent requests all miss simultaneously and slam the database at once — a *stampede* (or *thundering herd*). Mitigations:

  • Locking / single-flight: only one request recomputes; others wait for the result.
  • Probabilistic early expiration: refresh slightly before the TTL with some randomness so keys don't all expire together.
  • stale-while-revalidate: serve the stale value while one worker refreshes.

For cold-start scenarios, cache warming (pre-populating hot keys on deploy) avoids a flood of misses right after a restart.

What not to cache

  • Highly personalized data with low reuse (caching per-user data nobody re-requests just wastes memory).
  • Data where staleness is unacceptable (auth/permission checks at the moment of action).
  • Anything you can't reliably invalidate and whose staleness causes correctness bugs.

And never cache sensitive data in shared/public caches — a Cache-Control: public on a personalized response can leak one user's data to another via a CDN.

Putting it together

A typical well-cached app: fingerprinted static assets cached forever at the CDN; HTML with short TTL + stale-while-revalidate; expensive query results in Redis with cache-aside and short TTLs plus write-time invalidation; sessions in Redis. Each layer absorbs load the next would otherwise bear.

Caching on PandaStack

PandaStack gives you the pieces this article relies on: a CDN/edge layer (Cloudflare DNS + Kong ingress) in front of your apps and static sites with automatic SSL, edge functions for logic at the edge, and managed Redis (via KubeBlocks) for an application cache or session store — auto-wired into your service. Static sites get served from edge cache by default, and you can layer Redis-backed app caching on top with the patterns above.

References

  • [MDN — HTTP caching](https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching)
  • [Redis — caching patterns and eviction](https://redis.io/docs/latest/develop/use/patterns/)
  • [RFC 9111 — HTTP Caching](https://www.rfc-editor.org/rfc/rfc9111.html)
  • [Cloudflare — what is caching](https://www.cloudflare.com/learning/cdn/what-is-caching/)
  • [stale-while-revalidate (web.dev)](https://web.dev/articles/stale-while-revalidate)

Want a CDN, edge functions, and managed Redis without stitching them together? PandaStack's free tier includes all three. [Start at dashboard.pandastack.io](https://dashboard.pandastack.io).

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Performance

Browse all Performance articles →

See also