Glossary

What is HTTP Caching (Cache-Control, ETag)?

May 20, 2026

HTTP caching is a mechanism in the HTTP protocol that lets browsers, CDNs and proxy servers store copies of responses and reuse them for later requests without contacting the origin server. It is the single most important performance technique on the web. A correctly cached static asset (image, CSS, JS bundle) takes 1 to 10 milliseconds to load from the browser cache versus 100 to 1000 milliseconds from the origin. HTTP caching is controlled by response headers defined in RFC 7234 (June 2014) and the newer RFC 9111 (June 2022): Cache-Control (the primary modern header), Expires (legacy), ETag and Last-Modified (validators), and Vary (cache key modifier). Used correctly, HTTP caching reduces Largest Contentful Paint (LCP), saves bandwidth (Google estimates 60 to 80 percent of typical website bandwidth could be cached), reduces origin server load and lowers carbon footprint. Used incorrectly, it causes stale content, broken deploys and infinite browser tab reloads.

How does HTTP caching work?

When a browser requests a resource, the response can include cache instructions. The browser stores the response locally. On the next request for the same URL, the browser checks the cache:

  1. If the cached copy is still fresh (within its TTL), the browser uses it directly without any network request. This is called a cache hit.
  2. If the cached copy is stale but has a validator (ETag or Last-Modified), the browser sends a conditional request with If-None-Match or If-Modified-Since. The server returns 304 Not Modified with no body if the resource has not changed, saving bandwidth.
  3. If there is no validator and the copy is stale, the browser refetches the full resource (cache miss).

Between the browser and the origin there are usually one or more shared caches (CDN edge nodes, corporate proxies). The same rules apply at each layer.

What does Cache-Control do?

Cache-Control is the primary header that controls caching behaviour. It accepts a comma separated list of directives:

DirectiveMeaning
max-age=NThe response is fresh for N seconds.
s-maxage=NSame as max-age but only for shared caches (CDN, proxy). Overrides max-age there.
publicAny cache may store the response (browser and shared).
privateOnly the browser may store, never shared caches. Use for personalized content.
no-cacheCache may store but must revalidate with the origin before reuse on every request.
no-storeNever store the response anywhere. For truly sensitive data (banking, health).
must-revalidateOnce stale, the cache must check with origin and cannot serve stale on errors.
stale-while-revalidate=NAfter expiry, serve stale for N seconds while revalidating in background. Big UX win.
stale-if-error=NIf origin returns 5xx, serve stale for up to N seconds.
immutableThe response will never change. The browser skips even validation on reload.

Practical examples:

# Static asset that is fingerprinted (app.abc123.js)
Cache-Control: public, max-age=31536000, immutable

# HTML page that should always be fresh but tolerates a quick revalidate
Cache-Control: public, max-age=0, s-maxage=300, stale-while-revalidate=86400

# Personalized account page
Cache-Control: private, no-cache

# Login form
Cache-Control: no-store

What is an ETag?

An ETag (Entity Tag) is an opaque identifier the server attaches to a response to identify a specific version of the resource. When the client sends a conditional request, it includes the ETag value in the If-None-Match header. If the current ETag still matches, the server returns 304 Not Modified with no body, saving bandwidth.

HTTP/1.1 200 OK
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Content-Type: image/jpeg
Content-Length: 89342

[image data]

# Later request
GET /image.jpg HTTP/1.1
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"

# Response
HTTP/1.1 304 Not Modified

ETags can be strong (byte for byte identical) or weak (semantically equivalent, prefixed with W/). Strong ETags are usually a hash of the content (SHA1, MD5, xxHash). Weak ETags are useful when minor formatting differences should not invalidate the cache.

What is Last-Modified?

Last-Modified is a timestamp validator with 1 second resolution. The client sends it back as If-Modified-Since:

HTTP/1.1 200 OK
Last-Modified: Wed, 21 Oct 2025 07:28:00 GMT

# Later
GET /article HTTP/1.1
If-Modified-Since: Wed, 21 Oct 2025 07:28:00 GMT

# Response
HTTP/1.1 304 Not Modified

Compared to ETag, Last-Modified is simpler but limited: anything changing more often than once per second cannot be distinguished. For database backed content (a WordPress post updated twice in the same minute) ETag is the better choice.

What is the Vary header?

Vary tells the cache that the response depends on certain request headers. The cache must store separate copies per unique combination of those headers. Common examples:

  • Vary: Accept-Encoding separates gzip, brotli and uncompressed copies.
  • Vary: Accept-Language separates English, German, French versions.
  • Vary: User-Agent separates desktop and mobile HTML. Avoid this, it causes cache explosion because every browser version creates a new key.
  • Vary: Cookie effectively disables shared caching for any response, because every user has unique cookies. Almost always wrong.

Browser cache vs CDN cache vs proxy cache

Cache layerLocationHonored by
Browser cacheLocal disk or memory of the user devicemax-age, no-cache, no-store, ETag, Last-Modified
CDN edge cacheCDN PoP near the user (Cloudflare, Fastly, Akamai, CloudFront)s-maxage, public, stale-while-revalidate, Surrogate-Key, custom Cache-Tag
Corporate proxyInside enterprise networkspublic, private (rarely used today)
Service Worker cacheInside the page via JavaScriptCustom logic, ignores HTTP headers unless you tell it to

Cache busting and fingerprinting

Long max-age (a year) plus immutable is great for performance, but how do you deploy a new version? The answer is fingerprinting: build tools (webpack, Vite, esbuild, Turbopack, Rollup) include a content hash in the filename. app.css becomes app.a1b2c3d4.css. When you change the file, the hash changes, the URL changes, the cache miss is automatic. HTML pages reference these fingerprinted assets, so HTML itself should NOT have a long cache. Pattern:

  • HTML: Cache-Control: public, max-age=0, s-maxage=60, stale-while-revalidate=86400
  • JS/CSS/fonts/images with hash: Cache-Control: public, max-age=31536000, immutable

HTTP caching in WordPress

WordPress core sends very weak cache headers out of the box. Best practice is to add headers via a caching plugin or at the web server level. Options:

  • WP Rocket: paid, sets browser caching headers, serves a precomputed HTML cache, supports stale-while-revalidate.
  • LiteSpeed Cache: free, integrates with LiteSpeed and OpenLiteSpeed servers, supports ESI for partial caching, also works as a CDN cache hook.
  • W3 Total Cache: free, mature, supports browser cache rules, object cache (Redis, Memcached) and CDN integration.
  • WP Super Cache: free, simple full page cache by Automattic.
  • Cloudflare APO (Automatic Platform Optimization): cache full HTML at the edge for $5/month, deals with WordPress logged in users via cookies.

nginx config example for WordPress:

location ~* \.(jpg|jpeg|png|gif|webp|avif|svg|ico|css|js|woff2)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
    access_log off;
}

location / {
    try_files $uri $uri/ /index.php?$args;
    add_header Cache-Control "public, max-age=0, s-maxage=600, stale-while-revalidate=86400";
}

Common HTTP caching mistakes

  • Setting long max-age on HTML: users see old content after deploys with no way to recover except hard reload.
  • Forgetting to set headers on static assets: every page load refetches the same logo, slowing things down.
  • Using Vary: User-Agent: causes cache key explosion at the CDN.
  • Mixing Set-Cookie with Cache-Control: public: shared caches sometimes store the cookie and leak it to other users.
  • Trusting Pragma: no-cache: this is an HTTP/1.0 request header, ignored by modern servers and many CDNs. Use Cache-Control instead.
  • Not setting headers at all: browsers fall back to heuristic caching (10 percent of Last-Modified age) which is unpredictable.
  • Cache poisoning: if the cache key does not include all relevant headers, an attacker can poison the cache with malicious content. Always use Vary for any request header that influences the response.

How do I test HTTP caching?

  • Browser DevTools Network tab: open the resource and check Size column. (memory cache) or (disk cache) means hit. 304 means revalidated. Look at Response Headers.
  • curl: curl -I https://example.com/style.css prints headers.
  • WebPageTest: shows cache scores per asset and grades the configuration.
  • Lighthouse: audits "Uses efficient cache policy on static assets".
  • RedBot (redbot.org): a free Mark Nottingham tool that analyses HTTP caching headers and warns about subtle mistakes.

What about HTTP/2 and HTTP/3?

HTTP/2 (RFC 7540, May 2015) and HTTP/3 (RFC 9114, June 2022) keep the same caching semantics but add Server Push (now deprecated in Chrome since 2022 in favor of Early Hints, RFC 8297). Early Hints (103 Early Hints) let servers preload critical resources before the main response arrives, complementing the cache rather than replacing it.

How does InspectWP help with HTTP caching?

InspectWP inspects the response headers of the HTML and all linked assets of every analyzed URL. The report flags missing Cache-Control headers, overly short max-age values on fingerprinted assets, missing ETag or Last-Modified validators and dangerous Vary combinations.

Check your WordPress site now

InspectWP analyzes your WordPress site for security issues, SEO problems, GDPR compliance, and performance — for free.

Analyze your site free