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
Hash Generator →
Generate MD5, SHA-1, SHA-256, and SHA-512 hashes of any text online. Free, no signup — all hashing runs entirely in your browser via the Web Crypto API, so nothing is uploaded.
Timestamp Converter →
Convert Unix timestamps to human-readable dates and vice versa. Supports seconds, milliseconds, ISO 8601, and every timezone. Free, no signup, runs entirely in your browser.
cURL Converter →
Convert cURL commands to Python (requests), JavaScript (fetch), Node.js, Go (net/http), PHP, or Java (HttpClient). Parses -H, -d, --data-urlencode, -F, -u, --json and more. 100% local.
SHA-256 Generator →
Compute SHA-256 (256-bit) hashes of text or files instantly online. The gold-standard cryptographic hash for signatures, blockchain, and integrity. Drag-and-drop file support; verify against a known SHA-256 in one click. 100% local.
SHA-1 Generator →
Generate SHA-1 (160-bit) hashes of any text or file online. Drag-and-drop file support for GB-scale checksums via Web Crypto API. Verify against a known SHA-1 in one click. 100% local.