RoadmapDay 44 / 80
System DesignMonth 3 · Week 9

Day 44: Client Caching Strategies & HTTP Caching Headers

Use HTTP caching headers correctly to skip network round trips entirely for unchanged data, layered underneath application-level caches like React Query.

Mark this day complete

Study

Concepts

Freshness vs validation: two different caching questions

`Cache-Control: max-age=300` answers "how long can the browser/CDN trust this response WITHOUT asking the server at all" — for that window, a cached response is used with zero network requests. Once max-age expires, the cache does not necessarily throw the response away — it can VALIDATE: send a conditional request with `If-None-Match: "<etag>"` (or `If-Modified-Since`), and the server replies `304 Not Modified` (no body at all) if the resource is unchanged, or a fresh `200` with a new body if it changed. A 304 still costs a round trip, but saves the (often much larger) response body transfer.

`ETag` is a hash/version fingerprint of the exact response content; `Last-Modified` is a timestamp — ETag is generally preferred where available since content-based hashing avoids edge cases like a resource being re-saved with identical content but a new mtime.

Layering: HTTP cache, then application cache, then component state

These layers are complementary, not competing: the HTTP cache (browser/CDN) avoids network requests entirely for freshness-window hits, or saves body transfer on a validated 304; an application-level cache like React Query (Day 28) sits ABOVE that, avoiding even the JS-level overhead of issuing a fetch call and deciding whether to show cached data instantly while revalidating; component state/memoization is the innermost layer, avoiding recomputation of already-fetched data. A well-designed system sets sensible HTTP headers on the server AND a sensible `staleTime` on the client — they solve overlapping but distinct problems (network transfer vs. UI-perceived freshness).

A crucial distinction for API responses containing user-specific or sensitive data: `Cache-Control: private` restricts caching to the browser only (not shared CDN/proxy caches), while `no-store` disables caching entirely — getting this wrong is a real security/correctness bug (a shared CDN caching one user's personalized/authenticated response and serving it to a different user).

See It

Visualizations

Visualization

A cached request lifecycle

Within max-age

served from cache, zero network requests

max-age expired

send conditional request with If-None-Match

Server: unchanged

304 Not Modified — no body transferred

Server: changed

200 OK with fresh body + new ETag

Build It

Code Examples

Correct caching headers on a server response

js
// Public, shareable data — cache aggressively, revalidate cheaply after
res.set({
  'Cache-Control': 'public, max-age=300, stale-while-revalidate=60',
  ETag: computeEtag(responseBody), // e.g. a hash of the serialized body
});
res.status(200).json(responseBody);

// User-specific / authenticated data — never let a shared cache store it
res.set({ 'Cache-Control': 'private, no-store' });
res.status(200).json(personalizedData);

The browser handling the conditional request automatically

js
// The browser's fetch/cache implementation does this transparently —
// you do not manually send If-None-Match; it reads the previous ETag
// from its own cache and attaches the conditional header for you.
const res = await fetch('/api/products/42');
// If unchanged since last time: browser gets a 304 internally,
// fetch() still resolves with the CACHED body and res.status will
// reflect 200 from the cache's perspective in most implementations —
// the key point: only headers were transferred over the network, not the body.

Remember

Key Takeaways

  • max-age controls how long a response is trusted WITHOUT any network request at all.
  • After max-age expires, a conditional request (If-None-Match/ETag) can get a 304 — saves body transfer, still costs a round trip.
  • ETag (content hash) is generally preferred over Last-Modified (timestamp) for correctness.
  • HTTP cache, application cache (React Query), and component memoization are complementary layers solving different costs.
  • private/no-store are correctness/security controls, not just performance knobs — get them wrong and you leak one user's data to another via a shared cache.

Do It

Practice

  1. 1Inspect the response headers of 3 real API calls (DevTools Network tab) on a site you use daily and identify their caching strategy.
  2. 2Configure Cache-Control and ETag correctly on a small Express/Next.js API route and verify a second request returns 304.
  3. 3Explain, in writing, the security bug that results from setting Cache-Control: public on a personalized, authenticated API response.