DevKits
Concept

HTTP Caching Explained — Cache-Control, Expires, ETags & CDN Headers

HTTP caching lets browsers and CDNs re-use responses without hitting the origin server. Cache-Control, Expires, ETag/If-None-Match, and Vary together form the caching contract. This guide explains each piece and when to use which strategy.

Last updated:

Core Concepts

Cache-Control: max-age (vs Expires)
max-age specifies a relative duration (seconds from now) that a response stays fresh. Expires specifies an absolute wall-clock date. Relative beats absolute — server clock drift makes Expires unreliable, and browsers can compute remaining freshness from max-age on the fly. Always set Cache-Control: max-age. Only add Expires as a legacy fallback for CDNs that don't understand Cache-Control.
public vs private vs no-cache vs no-store
public: any cache (browser + CDN) may store the response. private: only the user's own browser cache (no shared CDN cache). no-cache: cache may store, but must revalidate before using (forces a conditional GET with If-None-Match every time). no-store: don't store at all — use for bank balances, auth cookies, and anything reflecting real-time user-specific data.
ETag and If-None-Match (validation)
An ETag is a fingerprint of the response body. When a cached response becomes stale (max-age passed), the browser sends If-None-Match: <etag> to ask 'has this changed?'. If the server returns 304 Not Modified with no body, the browser uses the cached copy. This saves bandwidth and CPU — perfect for large API responses that rarely change.
immutable + far-future max-age
For static assets with content-hash filenames (e.g. main.a3f6d.js), add Cache-Control: public, max-age=31536000, immutable. immutable tells the browser: 'If the user clicks refresh, don't even revalidate — I promise this file has not changed.' It eliminates conditional GETs on reload, reducing server load to zero for CDN-served assets.
Vary header
Vary tells caches to store multiple copies of a response based on request headers. For example, Vary: Accept-Encoding lets a CDN serve gzip vs brotli based on what the client supports. Vary: Origin is critical for CORS — without it, a CDN might serve the wrong CORS headers across origins.

Frequently Asked Questions

What's the difference between Expires and Cache-Control: max-age?

Expires is an absolute point in time (e.g. 'Wed, 21 Oct 2025 07:28:00 GMT'). Cache-Control: max-age is a relative duration (e.g. 3600 seconds from the response date). Use max-age — it works even when server and client clocks disagree. If both are present, max-age takes priority per RFC 9111.

How do I bust the cache when I deploy new code?

Use content-hash filenames (main.abc123.js), not query strings. Query strings are ignored by some proxy caches. Content hashes, combined with immutable, guarantee seamless cache updates: the new HTML references the new JS hash, old HTML still hits the old hash until it expires.

Try these related tools