HTTP Cache-Control Headers Explained

Define the engineering scope: precise directive syntax, browser/CDN parsing precedence, and measurable impact on Core Web Vitals. This guide prioritizes actionable configuration over theoretical definitions, establishing diagnostic workflows and explicit performance thresholds for frontend asset delivery. Cache-Control operates as the single source of truth for cache lifecycle management across origin servers, edge networks, and client browsers. Misconfiguration directly degrades Time to First Byte (TTFB), inflates bandwidth costs, and introduces cache poisoning vectors. The following sections provide production-ready templates, validation scripts, and metric-driven optimization targets.

Directive Architecture and Browser Parsing Precedence

The Cache-Control header uses a comma-separated list of directives that dictate how intermediaries and user agents store, validate, and serve responses. Modern browsers resolve conflicting instructions through a strict precedence hierarchy: no-store overrides all caching, private restricts storage to the local browser, and public permits shared caching across CDNs and proxies.

max-age defines the freshness lifetime in seconds relative to the response timestamp. s-maxage overrides max-age exclusively for shared caches, allowing you to serve highly personalized content to users while aggressively caching identical payloads at the edge. must-revalidate forces the browser to contact the origin once max-age expires, preventing stale content delivery during offline transitions. Understanding how these directives propagate through multi-tier architectures is critical for maintaining consistency across Advanced Caching Strategies & CDN Architecture implementations.

Diagnostic Workflow: Use the DevTools Network panel to compare response headers across initial load and hard-reload states. Verify directive precedence using curl -I with explicit Accept headers to simulate different client profiles.

bash
curl -I -H "Accept: text/html" https://your-domain.com/
curl -I -H "Accept: application/json" https://your-domain.com/api/data

Performance Thresholds: Target <50ms TTFB for cached static assets. Enforce max-age >= 31536000 (1 year) for immutable resources to eliminate validation overhead.

Asset-Specific Configuration Templates

Cache directives must align with asset volatility and delivery patterns. Applying uniform headers across all routes guarantees either stale content delivery or unnecessary origin hits. Implement route-level middleware or server configuration blocks to inject precise combinations.

Asset TypeRecommended DirectiveRationale
HTML Documentsmax-age=0, must-revalidate, no-cacheEnsures navigation always checks for updated DOM structure and routing logic.
Hashed JS/CSSmax-age=31536000, public, immutableContent-hashed filenames guarantee uniqueness. Browsers skip validation entirely.
Unhashed Imagesmax-age=2592000, public30-day TTL balances freshness with CDN edge efficiency for media assets.
API JSONmax-age=300, private, no-transformShort TTL for dynamic data; private prevents shared caching of user-specific payloads.

When detailing versioned static asset strategies, align build-time hashing with Setting up immutable cache headers for hashed assets to prevent stale bundle delivery.

Diagnostic Workflow: Audit your build pipeline output to verify filename hashing matches immutable flags. Run automated header checks against staging environments before promotion.

Vite/Webpack Configuration (JSON):

json
{
 "build": {
 "rollupOptions": {
 "output": {
 "assetFileNames": "assets/[name]-[hash][extname]",
 "chunkFileNames": "assets/[name]-[hash].js",
 "entryFileNames": "assets/[name]-[hash].js"
 }
 }
 }
}

Express.js Middleware (JavaScript):

javascript
app.use((req, res, next) => {
 const path = req.path;
 if (/\.[a-f0-9]{8,}\.(js|css)$/.test(path)) {
 res.set('Cache-Control', 'public, max-age=31536000, immutable');
 } else if (path.startsWith('/api/')) {
 res.set('Cache-Control', 'private, max-age=300, no-transform');
 } else if (path.endsWith('.html')) {
 res.set('Cache-Control', 'no-cache, must-revalidate');
 }
 next();
});

CDN and Reverse Proxy Interaction Models

CDNs (Cloudflare, Fastly, AWS CloudFront) and reverse proxies (Nginx, HAProxy) interpret Cache-Control directives but often apply their own override rules. By default, many CDNs strip Set-Cookie and ignore caching for responses containing authentication headers. To enforce origin directives at the edge, explicitly configure cache keys, disable dashboard TTL overrides, and leverage s-maxage for shared cache control.

When mapping origin directives to edge cache keys, TTL overrides, and bypass rules for authenticated routes, reference CDN Edge Caching Configuration for architecture-specific tuning.

Diagnostic Workflow: Compare origin vs edge response headers using X-Cache-Status or CF-Cache-Status. Test cache bypass triggers by appending query strings or modifying Vary headers to ensure deterministic routing.

bash
curl -sI https://your-domain.com/static/app-abc123.js | grep -E "Cache-Control|CF-Cache-Status|Age"

Nginx Configuration:

nginx
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$ {
 expires 1y;
 add_header Cache-Control "public, max-age=31536000, immutable";
 add_header Vary "Accept-Encoding";
 access_log off;
}

location /api/ {
 proxy_pass http://backend;
 proxy_cache_valid 200 5m;
 add_header Cache-Control "private, max-age=300, no-transform";
}

Performance Thresholds: Maintain edge cache hit ratio >85% for static assets. Achieve origin request reduction >70% after directive alignment and proxy rule optimization.

Diagnostic Workflows and Cache Validation Testing

Validation requires automated, repeatable checks integrated into CI/CD pipelines. Manual inspection scales poorly across microservices and multi-region deployments. Implement scripts that parse response headers, flag missing directives, and validate Age header progression to confirm edge caching is active.

Cache state directly impacts resource hint efficacy and network queue prioritization. When Cache-Control forces revalidation, preloaded assets may compete with critical rendering paths. Review Using resource hints: prefetch vs prerender vs preconnect to align hint strategies with your cache lifecycle.

Diagnostic Workflow: Execute CI/CD pipeline scripts that parse headers and simulate offline/airplane mode to test stale-while-revalidate fallback behavior.

bash
#!/bin/bash
# cache-audit.sh
URLS=("https://example.com/" "https://example.com/static/main.js" "https://example.com/api/config")
for url in "${URLS[@]}"; do
 HEADERS=$(curl -sI "$url")
 echo "=== $url ==="
 echo "$HEADERS" | grep -E "Cache-Control|Age|X-Cache-Status|ETag"
 echo ""
done

Performance Thresholds: Zero cache-poisoning incidents (validated via Vary header audit). Maintain <200ms validation latency for conditional requests using If-None-Match / If-Modified-Since.

Metric Optimization Thresholds and Performance Budgets

Cache configuration directly dictates bandwidth consumption and Core Web Vitals. Establish explicit targets: cache hit ratio >85%, TTFB <100ms for cached assets, and Largest Contentful Paint (LCP) improvement >=15% post-optimization. Calculate bandwidth savings using: Savings (%) = (1 - (Cached_Requests / Total_Requests)) * 100

Private scoping and cookie stripping significantly affect cacheability and request overhead. When Cache-Control: private is applied, intermediaries bypass storage, but reducing cookie payload size for faster requests](/advanced-caching-strategies-cdn-architecture/http-cache-control-headers-explained/reducing-cookie-payload-size-for-faster-requests/) ensures that even dynamic endpoints minimize network latency and avoid unnecessary cache fragmentation.

Diagnostic Workflow: Correlate Real User Monitoring (RUM) data from CrUX and WebPageTest with cache configuration changes. Track LCP delta before and after directive optimization using Lighthouse CI.

Performance Thresholds: LCP improvement >=15% post-optimization. Bandwidth cost reduction >=40% via aggressive static asset caching and edge offloading.

Advanced Patterns: SWR, Immutable, and Invalidation

stale-while-revalidate (swr) decouples content delivery from freshness validation. The browser serves cached content instantly while fetching an updated version in the background. This pattern is critical for high-traffic marketing pages and dashboards where sub-second perceived latency outweighs strict consistency requirements. Pair swr with max-age to define the stale window: Cache-Control: public, max-age=3600, stale-while-revalidate=86400.

The immutable directive signals that the resource will never change during its freshness lifetime. Browser support is universal in modern engines, but it requires strict content-hashed filenames. When immutable conflicts with Service Worker fetch handlers, ensure your Service Worker Caching Strategies explicitly bypass network fallback for hashed routes to prevent double-fetching.

Diagnostic Workflow: Test swr background fetch concurrency limits using Chrome DevTools > Network > Throttling. Verify cache invalidation via ETag/Last-Modified fallback when max-age expires.

Performance Thresholds: swr background fetch timeout <=500ms. Cache invalidation propagation <2 seconds across edge nodes after origin purge.

Common Implementation Pitfalls

  • Over-caching HTML/APIs: Applying max-age=31536000 to HTML or dynamic JSON endpoints causes stale content delivery without revalidation, breaking routing and personalization.
  • no-cache vs no-store Confusion: no-cache stores the response but requires validation before reuse. no-store bypasses caching entirely. Misapplying these destroys performance budgets.
  • Missing Vary Headers: Omitting Vary: Accept-Encoding or Vary: User-Agent when serving compressed or device-specific content leads to cache poisoning and broken layouts.
  • Dashboard TTL Overrides: Relying on CDN UI overrides without aligning origin Cache-Control causes cache inconsistency across edge nodes and complicates debugging.
  • immutable Without Hashing: Setting immutable on non-hashed filenames results in permanent delivery of outdated assets until manual cache purges are executed.

Frequently Asked Questions

How does Cache-Control: max-age=0 differ from no-cache? Both directives force revalidation, but max-age=0 allows the browser to store the response and check freshness immediately on subsequent requests. no-cache explicitly requires origin validation before serving any cached copy. Use max-age=0, must-revalidate for HTML to enable conditional requests (304 Not Modified), and reserve no-cache for highly sensitive payloads where storage is permitted but validation is mandatory.

Can I safely use Cache-Control: immutable for all versioned assets? Yes, provided you enforce content-hashed filenames at build time. immutable tells the browser to skip validation entirely for the duration of max-age. If a 404 occurs due to a broken hash reference, the browser will fallback to a network request. Pair immutable with strict build pipeline validation and automated 404 monitoring to prevent broken asset delivery.

Why is my CDN ignoring my origin Cache-Control headers? CDNs often apply default caching rules based on status codes, file extensions, or dashboard configurations. Verify that 302 redirects or 4xx responses aren't being cached by default. Ensure Vary headers align with edge cache keys, disable automatic TTL overrides in your CDN dashboard, and confirm your origin isn't stripping headers via proxy middleware.

How does stale-while-revalidate impact LCP and TTFB?swr serves cached content instantly, typically achieving sub-50ms TTFB while fetching fresh data asynchronously. This decouples perceived performance from network latency, directly improving LCP for above-the-fold assets. Apply swr when content volatility is low-to-medium and user session state doesn't require strict consistency. For highly dynamic data, prefer strict max-age with short TTLs and background polling.

  • Reducing cookie payload size for faster requests