HTTP caching is a massive, invisible optimization that makes the web feel instant, but it’s not about not downloading things. It’s about downloading less and faster by leveraging intelligent conditional requests.
Let’s see it in action. Imagine a browser requesting an image (/logo.png).
-
Initial Request:
GET /logo.png HTTP/1.1 Host: example.com -
Server Response:
HTTP/1.1 200 OK Content-Type: image/png Content-Length: 15000 ETag: "abcdef12345" Cache-Control: public, max-age=3600 Last-Modified: Tue, 15 Nov 2023 10:00:00 GMT <binary image data>The browser stores this image along with the
ETagandCache-Controlheaders. -
Subsequent Request (within 1 hour):
GET /logo.png HTTP/1.1 Host: example.com If-None-Match: "abcdef12345"Notice
If-None-Match. The browser is saying, "If the ETag is stillabcdef12345, don’t bother sending the whole file." -
Server Response (if unchanged):
HTTP/1.1 304 Not Modified ETag: "abcdef12345" Cache-Control: public, max-age=3600 <no body data>The browser knows it has the latest version and uses its cached copy. This is a "cache hit." If the ETag had changed, the server would send a
200 OKwith the new image data.
This system solves the problem of redundant data transfer over the network. Every time a user revisits a page or reloads an asset, the browser can avoid re-downloading identical content by asking the server, "Do you still have this specific version?" The most common way to track "this specific version" is using ETag (Entity Tag) headers. An ETag is an opaque identifier assigned by the server to a specific version of a resource. It can be a hash of the content, a version number, or any string that uniquely identifies the representation. When the browser makes a subsequent request, it sends the ETag it has stored in an If-None-Match header. The server compares this to the ETag of the current version of the resource. If they match, the server returns a 304 Not Modified status code with an empty body, saving bandwidth and time. If they don’t match, it means the resource has changed, and the server sends back the new version with a 200 OK status.
Cache-Control is the primary directive for how caching should be handled. public means any cache (browser, proxy, CDN) can store it. private means only the end-user’s browser can cache it. max-age=3600 tells caches that the resource is fresh for 3600 seconds (1 hour) from the time it was generated by the server. After max-age expires, the cache must revalidate with the server. no-cache doesn’t mean "don’t cache," it means "always revalidate before using the cached version." no-store truly means "don’t cache at all."
The Last-Modified header is an older, less precise mechanism but still widely used. It indicates the date and time the resource was last modified on the server. The browser can send this in an If-Modified-Since header. The server compares the date. The problem is that if a file is modified and then modified again within the same second, Last-Modified might not change, leading to stale content. ETag is superior because it can represent any versioning scheme, not just time.
The Vary header is crucial for intermediate caches like CDNs or reverse proxies. It tells the cache that the response depends on certain request headers. For example, Vary: Accept-Encoding means that a compressed version (e.g., gzip) and an uncompressed version of the same resource should be treated as distinct cache entries by the intermediate cache. If a request comes in with Accept-Encoding: gzip, the cache will serve the gzipped version. If another request comes in with Accept-Encoding: br, it must fetch the br-compressed version (or uncompressed if available), rather than serving the gzipped one from its cache. Other common Vary values include User-Agent (for mobile vs. desktop versions) or Accept-Language (for different language versions). Without Vary, an intermediate cache might incorrectly serve a gzip-compressed image to a browser that only understands br, or serve a desktop-formatted page to a mobile user.
The most surprising thing about ETag and Last-Modified is that they are validation mechanisms, not expiration mechanisms. Cache-Control: max-age dictates when a cache thinks a resource might be stale and needs to check. ETag and Last-Modified are the tools used during that check to confirm if the resource actually changed. A cache can hold a resource indefinitely if it never receives a request that triggers revalidation, regardless of its max-age.
The next problem you’ll encounter is understanding how Cache-Control directives interact and the subtle differences between no-cache and no-store.