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.
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
served from cache, zero network requests
send conditional request with If-None-Match
304 Not Modified — no body transferred
200 OK with fresh body + new ETag
Build It
Code Examples
Correct caching headers on a server response
// 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
// 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
- 1Inspect the response headers of 3 real API calls (DevTools Network tab) on a site you use daily and identify their caching strategy.
- 2Configure Cache-Control and ETag correctly on a small Express/Next.js API route and verify a second request returns 304.
- 3Explain, in writing, the security bug that results from setting Cache-Control: public on a personalized, authenticated API response.