HTTP Caching Guide: Cache-Control, ETag & CDN Tuning

Web/SEO

HTTP Caching: The First Line of Defense in Performance Optimization

With properly configured caching, repeat visits require zero network requests, and load times drop from seconds to milliseconds.

Cache Type Mechanism Hits the Server? Speed
Strong cache Cache-Control / Expires No Fastest (0ms)
Negotiated cache ETag / Last-Modified Yes (conditional request) Fast (304 response)
No cache no-cache / no-store Yes (full request) Slowest

1. Strong Caching

Cache-Control Directives

Cache-Control: max-age=31536000, immutable
Directive Effect Use Case
max-age=N Cache for N seconds Static assets
s-maxage=N CDN caches for N seconds (overrides max-age) CDN scenarios
immutable Never revalidate, even after expiry Files with hashed names
public Allow intermediate caches CDN
private Browser cache only Personalized content
no-cache Must revalidate before each use HTML pages
no-store Never cache at all Sensitive data
must-revalidate Must revalidate once expired API responses
stale-while-revalidate=N Serve stale content while refreshing in the background Progressive updates

Expires (Legacy Standard)

Expires: Thu, 02 Jun 2027 00:00:00 GMT

Priority: Cache-Control: max-age > Expires.


2. Negotiated Caching

ETag / If-None-Match

# First request
GET /api/data
→ 200 OK
  ETag: "abc123"
  Cache-Control: max-age=0

# Subsequent request (conditional)
GET /api/data
If-None-Match: "abc123"
→ 304 Not Modified  (no response body, saves bandwidth)

Last-Modified / If-Modified-Since

# First request
GET /style.css
→ 200 OK
  Last-Modified: Mon, 01 Jun 2026 12:00:00 GMT

# Subsequent request
GET /style.css
If-Modified-Since: Mon, 01 Jun 2026 12:00:00 GMT
→ 304 Not Modified

ETag vs Last-Modified

Dimension ETag Last-Modified
Precision Byte-level Second-level
Performance Requires hash computation Reads file timestamp directly
Reliability High Low (misses multiple changes within 1 second)
Distributed setups Requires consistent hashing Naturally consistent

Recommendation: Prefer ETag.


3. Caching Strategy Matrix

HTML Pages

Cache-Control: no-cache

HTML is the entry file — it must be revalidated every time so users always get the latest version.

Static Assets with Content Hash

Cache-Control: max-age=31536000, immutable

When the filename contains a hash (e.g. app.abc123.js), any content change produces a new hash → new URL → new request. The old URL never changes, so it can be cached for a year and marked immutable.

Static Assets without Hash

Cache-Control: max-age=86400, stale-while-revalidate=604800

After expiry, the stale version is served while the cache silently refreshes in the background — users never notice.

API Responses

Cache-Control: private, max-age=0, must-revalidate

API responses usually contain user data. private prevents CDN caching, and must-revalidate ensures stale values are never served after expiry.


4. The Vary Header: Extending the Cache Key

Vary: Accept-Encoding, Accept-Language

Vary tells caches: besides the URL, also distinguish cached versions by the values of these request headers.

Vary Value Meaning Scenario
Accept-Encoding Cache per compression method gzip vs br
Accept-Language Cache per language Multilingual sites
Cookie Cache per cookie Personalized pages
* Every request cached independently Equivalent to no caching

Caution: Vary: Cookie leads to an extremely low cache hit rate — use with care.


5. CDN Caching Strategies

Origin-Pull Control

# CDN caches for 1 day, browser caches for 5 minutes
Cache-Control: public, s-maxage=86400, max-age=300
Scenario s-maxage max-age Notes
Static assets 31536000 31536000 Long cache on both CDN and browser
API responses 300 0 CDN caches 5 minutes, browser doesn't cache
HTML 60 0 CDN caches 1 minute, browser doesn't cache
Personalized content — 0 Neither CDN nor browser caches

CDN Cache Purging

Method Speed Best For
URL prewarming Fast New content launches
Full purge Slow Emergency fixes
Directory purge Medium Bulk updates
Cache-Control: no-cache Automatic API scenarios

Stale-While-Revalidate in Practice

Cache-Control: max-age=60, stale-while-revalidate=86400
Second 0:   request → cache MISS → origin pull → fresh value returned
Second 30:  request → cache HIT → cached value returned
Second 90:  request → cache STALE → stale value returned immediately + background origin pull
Second 91:  background pull completes → cache updated
Second 120: request → cache HIT → new value returned

Result: Users never wait for an origin pull; updates happen silently in the background.


6. Cache-Busting Strategies

<!-- Hash generated at build time -->
<script src="/app.abc123.js"></script>
<link href="/style.def456.css">
// webpack configuration
output: {
  filename: '[name].[contenthash:8].js',
}

Version Query Parameter

<!-- Simple, but some CDNs may not cache ?v= parameters -->
<script src="/app.js?v=2.0.0"></script>

Problem: Some CDNs ignore query parameters, causing stale caches.

<!-- Changes every time, completely breaking the cache -->
<script src="/app.js?t=1717315200"></script>

7. Service Worker Caching

Cache First Strategy

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request)
      .then(cached => cached || fetch(event.request))
  );
});

Stale While Revalidate Strategy

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.open('dynamic').then(cache =>
      cache.match(event.request).then(cached => {
        const fetchPromise = fetch(event.request)
          .then(response => {
            cache.put(event.request, response.clone());
            return response;
          });
        return cached || fetchPromise;
      })
    )
  );
});

Choosing a Strategy

Strategy Primary Source Best For
Cache First Cache Fonts, images
Network First Network APIs, HTML
Stale While Revalidate Cache + background update CSS, JS
Network Only Network Non-GET requests
Cache Only Cache Offline pages

8. Debugging Caches

Chrome DevTools

  1. DevTools → Network panel
  2. Check the Size column:
    • from disk cache: disk cache hit
    • from memory cache: memory cache hit
    • Actual size: cache miss
  3. Check the Age response header: how long the cached copy has existed
  4. Enable Disable cache: disables caching (while DevTools is open)

Verifying with curl

# Inspect response headers
curl -I https://example.com/style.css

# Send a conditional request
curl -I -H 'If-None-Match: "abc123"' https://example.com/style.css
# 304 means negotiated cache hit

# Simulate different Accept-Encoding values
curl -I -H 'Accept-Encoding: br' https://example.com/style.css

9. Best Practices Checklist

  1. HTML: no-cache — keep the entry file always fresh
  2. Hashed JS/CSS: max-age=1 year + immutable
  3. APIs: must-revalidate — always revalidate after expiry
  4. CDN: s-maxage to control origin-pull frequency
  5. Vary: Accept-Encoding to distinguish compression formats
  6. Use ETag instead of Last-Modified
  7. stale-while-revalidate for seamless updates
  8. Service Worker to add offline capability
#HTTP缓存#Cache-Control#ETag#CDN#性能优化