HTTP caching is the single most effective performance optimization in distributed web architecture. A well-configured caching strategy drops origin server loads by 90%+, slashes Time to First Byte (TTFB) to single-digit milliseconds at the CDN edge, and reduces user bandwidth consumption.
However, improper caching configurations lead to disastrous consequences: users seeing outdated JavaScript bundles, authenticated account data leaking into public edge caches, or cascading cache-stampedes bringing down databases.
This guide provides a comprehensive, production-tested manual for configuring Cache-Control, ETag, stale-while-revalidate, and CDN-specific edge headers.
1. The Two-Tier Caching Architecture: Private vs Shared
Understanding where an HTTP response lives in the network path is essential before writing any Cache-Control directives.
+----------------+ +-------------------+ +-------------------+ +----------------+
| User Browser | <--> | CDN / Edge Proxy | <--> | Reverse Proxy/API | <--> | Origin Server |
| (Private Cache)| | (Shared Cache) | | (Nginx/Caddy) | | (Node/Go/Rails)|
+----------------+ +-------------------+ +-------------------+ +----------------+
1. Private Caches (Browser-Only)
- Target: The individual user's local disk/memory cache.
- Rule: Responses containing personalized user data, session tokens, or private settings must never be cached in shared proxies.
- Header:
Cache-Control: private, max-age=300
2. Shared Caches (CDNs & Gateways)
- Target: Intermediate edge networks (Cloudflare, AWS CloudFront, Fastly, Varnish, Nginx).
- Rule: Responses are shared across millions of unrelated end users.
- Header:
Cache-Control: public, s-maxage=3600, max-age=60
2. Core Cache-Control Directives Cheat Sheet
The Cache-Control header (standardized in RFC 9111) accepts a comma-separated list of instructions:
| Directive | Cache Type | Meaning | Common Use Case |
|---|---|---|---|
no-store |
Browser & CDN | Do not cache anything. Discard response immediately. | Sensitive endpoints, auth tokens, checkout APIs. |
no-cache |
Browser & CDN | Cache the response, but always revalidate with origin before serving. | Dynamic HTML documents, frequently updated configuration. |
public |
Browser & CDN | Explicitly allows public shared proxies to store the response. | Public static assets, unauthenticated catalog APIs. |
private |
Browser only | Shared caches (CDNs) must not store this. Only the end user's browser may. | User dashboard JSON, user profile photos. |
max-age=<sec> |
Browser & CDN | Response is fresh for N seconds from generation. |
Static assets, image assets. |
s-maxage=<sec> |
CDN / Shared only | Overrides max-age specifically for shared caches (CDNs). |
Edge-cached APIs with short browser lifetimes. |
stale-while-revalidate=<sec> |
Browser & CDN | Serve stale cached data instantly while refreshing in background. | High-traffic read-heavy APIs, blogs, news feeds. |
stale-if-error=<sec> |
Browser & CDN | Serve stale data if the origin server returns a 5xx error. | High-availability resilience during origin outages. |
must-revalidate |
Browser & CDN | Once stale, the cache must not serve the response without revalidating. | Financial data, inventory counts. |
immutable |
Browser only | The response body will never change during its max-age. Never send 304 checks. |
Content-hashed bundles (app.a7f3c9.js, style.4b1e.css). |
3. The stale-while-revalidate (SWR) Pattern
The stale-while-revalidate directive decouples user latency from cache validation.
When a client requests a resource:
- If the resource is within
max-age, the cache serves it immediately (Cache HIT). - If the resource has exceeded
max-agebut is withinstale-while-revalidatewindow:- The cache immediately serves the stale cached copy (0ms latency penalty).
- In the background, the edge proxy asynchronously fires a revalidation request to the origin server.
- When the origin responds, the cache updates its stored copy for subsequent users.
- If the resource has exceeded both windows, the request blocks on the origin (Cache MISS).
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: public, max-age=60, stale-while-revalidate=86400
ETag: W/"v2-9481a0"
Timeline:
0s ------------ 60s -------------------------------- 86460s ---------->
|-- Fresh (Hit) --|-- Stale (Instant Serve + BG Fetch) --|-- Expired --|
4. Conditional Validation: ETags vs Last-Modified
When a cached response becomes stale, the browser or CDN can check if the underlying resource actually changed rather than downloading the entire payload again.
Strong vs Weak ETags
- Strong ETag (
"abc123xyz"): Byte-for-byte identity. If even one whitespace changes or compression algorithm differs (gzip vs brotli), the tag must change. - Weak ETag (
W/"abc123xyz"): Semantic equivalence. The content is functionally identical even if byte representation differs slightly.
Client Server
| |
|--- GET /api/v1/pricing ---------->|
|<-- 200 OK (ETag: "7f8b9c") -------| (Stores in local cache)
| |
| [Resource expires after max-age] |
| |
|--- GET /api/v1/pricing ---------->|
| If-None-Match: "7f8b9c" | (Revalidation request)
| |
|<-- 304 Not Modified --------------| (Header only, 0 byte body!)
Fast ETag Generation in Node.js / Express
import crypto from 'node:crypto';
import express from 'express';
const app = express();
function generateWeakETag(body: string | Buffer): string {
const hash = crypto.createHash('sha1').update(body).digest('base64url').slice(0, 16);
return `W/"${hash}"`;
}
app.get('/api/catalog', (req, res) => {
const data = JSON.stringify(getCatalogData());
const etag = generateWeakETag(data);
res.setHeader('Cache-Control', 'public, max-age=120, stale-while-revalidate=600');
res.setHeader('ETag', etag);
if (req.headers['if-none-match'] === etag) {
return res.status(304).end();
}
res.setHeader('Content-Type', 'application/json');
return res.send(data);
});
5. Standard Production Caching Recipes
Recipe A: Hashed Static Assets (Vite, Next.js, Webpack)
Static JavaScript chunks, CSS files, and fonts with content hashes in their filename (/assets/chunk-8f92ab.js) should be cached permanently:
Cache-Control: public, max-age=31536000, immutable
Recipe B: Dynamic HTML Pages (/index.html, SSR routes)
HTML pages should never be cached permanently in browsers because they contain references to new asset hashes:
Cache-Control: public, max-age=0, must-revalidate
(Or s-maxage=600, stale-while-revalidate=3600 if caching at the CDN edge while forcing browsers to recheck).
Recipe C: Authenticated User APIs (/api/me, /api/orders)
Avoid leaking private data into shared proxies:
Cache-Control: private, no-cache, no-store, must-revalidate
Pragma: no-cache
Recipe D: High-Traffic Read APIs (Product catalogs, public profiles)
Fast edge delivery with zero downtime during database spikes:
Cache-Control: public, max-age=15, s-maxage=300, stale-while-revalidate=3600, stale-if-error=86400
6. Targeted CDN Headers: CDN-Cache-Control & Surrogate-Control
Modern edge platforms support targeted directives to control CDN edge caching independently from downstream browser caching.
# Tells browser: do not cache locally
# Tells CDN edge: cache for 1 hour and allow 1 day stale-while-revalidate
Cache-Control: private, no-cache
CDN-Cache-Control: max-age=3600, stale-while-revalidate=86400
Provider-specific override precedence:
Cloudflare-CDN-Cache-Control/Fastly-Key/Surrogate-Control(Highest priority)CDN-Cache-Control(Standardized cross-CDN header, RFC 9213)Cache-Controls-maxageCache-Controlmax-age(Lowest priority)
7. How to Debug Caching Headers with cURL
Inspect caching response headers, verify 304 revalidation, and inspect CDN cache status (HIT, MISS, EXPIRED):
# 1. Inspect headers including Cloudflare / CloudFront cache status
curl -I -s https://api.example.com/v1/products \
-H "Accept-Encoding: gzip, br"
# Look for:
# Cache-Control: public, max-age=60, s-maxage=3600
# ETag: W/"89f1a23c"
# CF-Cache-Status: HIT
# Age: 42
# 2. Test conditional revalidation (Expect HTTP 304)
curl -I -s https://api.example.com/v1/products \
-H 'If-None-Match: W/"89f1a23c"'
Use the HTTP Headers Analyzer to inspect your production endpoints, validate missing security/caching headers, and simulate CDN edge behavior.