HTTP caching is the mechanism for storing responses so the same resource isn't downloaded again and again. Done well, it speeds up responses, cuts server load and bandwidth, and even helps offline. Two ideas make it click: how long a response stays fresh (freshness) and how to check whether a stale copy is still valid (validation).
Cache-Control: controlling freshness
The Cache-Control response header is the heart of caching policy. The common directives are:
max-age=SECONDS: How many seconds the response counts as fresh. Within that window it's served straight from cache with no network request.no-cache: Store it, but always revalidate with the server before reuse. Note this is *not* 'do not cache.'no-store: Do not store at all. Use for sensitive personal data.private/public:privatemeans only a user-specific cache like the browser;publicallows shared caches such as CDNs and proxies to store it.immutable: The content will never change, so don't revalidate while fresh — ideal for hashed static assets.
HTTP/1.1 200 OK
Cache-Control: public, max-age=3600
ETag: "a1b2c3d4"
Content-Type: text/css
Revalidation: ETag and Last-Modified
Once the fresh period passes, a cache doesn't throw the copy away — it asks the server 'is this still valid?' There are two conditional-request styles. ETag is a fingerprint (version identifier) of the resource's content; Last-Modified is its last change time. The browser sends the stored ETag back in If-None-Match and the Last-Modified value in If-Modified-Since.
GET /style.css HTTP/1.1
If-None-Match: "a1b2c3d4"
HTTP/1.1 304 Not Modified
ETag: "a1b2c3d4"
Cache-Control: public, max-age=3600
If nothing changed, the server replies with just 304 Not Modified and no body. The client reuses the copy it already had, and the transfer shrinks to a few header lines. If the content did change, the server returns 200 with the full body and a new ETag.
Browser cache, CDNs, and cache busting
Caches live in several layers: the user's browser cache and, in front of it, shared caches like CDNs and reverse proxies. A response marked public can be stored by a CDN and served quickly from a nearby edge to users worldwide. The catch: a long max-age means users keep seeing an old version even after you ship new content.
The standard remedy is cache busting: embed a content hash in the filename or query string (app.9f3a2b.js) so the URL itself changes whenever the content does. A new URL is treated as uncached, so the fresh file is fetched, while unchanged files can be cached for a year with max-age=31536000, immutable.
Vary: same URL, different responses
The same URL can produce different responses depending on request headers — gzip vs identity based on Accept-Encoding, or different languages based on Accept-Language. The Vary response header tells caches 'this response depends on the listed request headers,' so a cache won't serve the wrong variant to another user. Forgetting Vary: Accept-Encoding can cause a gzip response to be handed to a client that can't decompress it.