Skip to content
Backend

The Caching Stack: From Browser to Buffer Pool

Five layers walked top to bottom — browser headers, CDN edge with its famous incident, Redis with a job description, database-adjacent options — plus the staleness grid that makes TTLs a product decision.

5 min read Updated Sep 4, 2026
The Caching Stack: From Browser to Buffer Pool

When someone says "we added caching", my first question is always "which caching?" — because a modern web request passes through at least five places where a cache could answer it, and they have wildly different economics. Confusing them is how teams end up with a Redis cluster doing work a CDN would do for free, or a CDN cheerfully serving user A's account page to user B (the genre's most famous incident, reproduced monthly somewhere on Earth). Let's walk the request path top to bottom and put each layer to its right work.

The caching stack: CDN and edge, HTTP caching, application-level Redis, and database-adjacent caches

Layer 1: the browser — the cache you configure with headers

The fastest request is one that never leaves the device. For fingerprinted static assets (Vite's app-8f3ka92.css), the answer is maximal and safe: Cache-Control: public, max-age=31536000, immutable — the filename changes when content does, so staleness is structurally impossible. For HTML documents, the opposite: no-cache (which means "revalidate", not "don't store") so deploys propagate instantly. The middle ground most teams miss: stale-while-revalidate, letting browsers serve slightly-old responses while refreshing in the background — free perceived latency for anything tolerant of seconds of staleness.

Layer 2: the CDN — free capacity, sharpest edges

The CDN absorbs traffic before it costs you compute — static assets trivially, and with care, whole anonymous pages: a blog like this one, product pages, landing pages can serve from edge nodes at effectively infinite scale. The two disciplines that keep it safe:

  • Never cache personalized responses at the edge without surgical key design. The B-sees-A's-page incident is always the same root cause: a Set-Cookie-bearing or session-varying response cached under a shared key. Blunt rule that's saved me repeatedly: authenticated traffic bypasses edge caching entirely (Cache-Control: private), and the edge caches only routes on an explicit allowlist.
  • Purge-by-tag or short TTLs — cache tags (Cloudflare, Fastly) let a publish event purge exactly the affected URLs; without them, keep edge TTLs short (60–300s) plus stale-while-revalidate, which for most content sites is indistinguishable from perfect invalidation at a fraction of the machinery.

Layer 3: the application — Redis, the workhorse with a job description

This is where the dedicated Redis post lives, so here's just the placement logic: the application cache exists for expensive computations over your own data — rendered fragments, aggregated dashboards, permission trees, external API responses. It is not for things an upper layer could hold (anonymous full pages → CDN) nor a bandage for things a lower layer should fix (a 900ms query that needs an index, not a cache — caching an unindexed query means the p99 on every miss is still 900ms, now with extra mystery). The honest test before adding any app-cache key: could the layer above hold this, or the layer below make this fast enough not to need holding? Only between those two answers does Redis earn the key.

Layer 4: database-adjacent caching — mostly, don't

MySQL's query cache is gone (removed in 8.0, deservedly — its invalidation granularity made it a contention machine), and the modern equivalents live elsewhere: the database's own buffer pool (which is why "the database is fast after warmup" — tune its size, it's your realest cache), materialized views / summary tables for heavy aggregations (the read-model pattern — refreshed on schedule or by events, honest about staleness), and read replicas — which aren't caches at all but absorb the same read pressure, with replication-lag semantics you must respect (read-your-own-writes routes to primary).

The cross-cutting law: staleness is a product decision

Every layer's TTL answers the same question — how stale is acceptable? — and that answer belongs to the product, not the infrastructure. My planning grid for any cacheable thing:

Data                      Tolerance   Layer(s)                  Invalidation
─────────────────────────────────────────────────────────────────────────────
Fingerprinted assets      infinite    browser + CDN             filename
Blog/article pages        minutes     CDN (+ SWR)               tag purge on publish
Product listing           ~60s        CDN short-TTL + app cache version stamp
User dashboard            seconds     app cache only            event-driven delete
Account/auth/checkout     zero        NO caching, ever          n/a
Search results            ~30s        app cache, keyed on query TTL only

Two rows deserve underlining. The "zero tolerance" row exists — some responses must never be cached anywhere, and marking them explicitly (private, no-store) is as important as caching the rest. And every row needs an owner for its invalidation story — a cache with no invalidation plan isn't a cache, it's a scheduled incident. Stack the layers with those two rules and you get the compounding win: the CDN absorbs the flood, Redis absorbs the computation, the buffer pool absorbs the reads — and the database finally gets to do only the work that's genuinely novel. Which was the whole idea.

Site slow despite "having caching"? The audit that finds which layer is missing — or lying — is a one-day engagement with before/after graphs included.

Keep reading

Related articles

Backend 4 min read

GraphQL: An Honest Take After the Hype Cycle

The specific problem it solves brilliantly, the five bills itemized — resolver N+1s, forfeited HTTP caching, query-surface DoS, field-level auth, the toolchain — and the BFF alternative most teams actually need.