HTTP caching is a cornerstone of web performance, but its intricacies often lead to surprising behavior. The most counterintuitive aspect is that a browser might ignore perfectly valid caching headers because of its own internal heuristics, prioritizing freshness over strict adherence to server instructions.
Let’s see this in action. Imagine a web server serving a static style.css file.
GET /style.css HTTP/1.1
Host: example.com
If-None-Match: "abcdef12345"
If-Modified-Since: Tue, 15 Nov 1994 12:45:26 GMT
HTTP/1.1 304 Not Modified
Date: Wed, 23 Oct 2023 10:00:00 GMT
ETag: "abcdef12345"
In this scenario, the browser already has style.css with the ETag "abcdef12345". It sends this ETag and the Last-Modified date in the If-None-Match and If-Modified-Since request headers, respectively. The server checks its current version of style.css. If it hasn’t changed, it responds with a 304 Not Modified and an empty body, telling the browser to use its cached copy. This is incredibly efficient, saving bandwidth and server processing.
The core problem HTTP caching headers solve is reducing latency and server load by allowing clients (browsers, proxies) to store and reuse resources locally. Instead of re-downloading an image or CSS file on every request, the client can check if the resource has changed. If not, it serves the local copy.
The key headers involved are:
-
Cache-Control: This is the primary directive header. It’s a powerful, flexible mechanism that allows a server to specify caching policies.public: Allows caching by any cache (browser, proxy, CDN).private: Allows caching only by the end-user’s browser.no-cache: Forces revalidation with the origin server before using a cached copy. It doesn’t mean "don’t cache," which is a common misconception.no-store: Disallows caching entirely. The response must not be stored in any cache.max-age=<seconds>: Specifies the maximum amount of time a resource is considered fresh. For example,max-age=3600means the resource is fresh for one hour.s-maxage=<seconds>: Similar tomax-age, but applies only to shared caches (like CDNs).must-revalidate: Tells caches that once a resource becomes stale, they must revalidate it with the origin server. They cannot serve a stale copy under any circumstances.proxy-revalidate: Similar tomust-revalidate, but only for shared caches.immutable: Indicates that the response body will not change over time. This is a strong hint to caches that they can serve the resource indefinitely without revalidation.
-
ETag(Entity Tag): An opaque identifier assigned by the web server to a specific version of a resource. It’s like a version number or a hash of the file content. When a client has a cached resource, it sends theETagin theIf-None-Matchrequest header. If theETagon the server matches the one in theIf-None-Matchheader, the server responds with304 Not Modified.Example:
ETag: "5f11098a3e7b3:0" -
Last-Modified: A timestamp indicating when the resource was last modified on the origin server. The client sends this in theIf-Modified-Sincerequest header. If the resource hasn’t been modified since that date, the server responds with304 Not Modified.Example:
Last-Modified: Tue, 15 Nov 1994 12:45:26 GMT
The interplay between these headers defines how a resource is cached. A common strategy is to set a long Cache-Control: max-age for static assets (like images, CSS, JS) that rarely change, combined with an ETag or Last-Modified for revalidation.
For dynamic content, you might use Cache-Control: no-cache to ensure the user always gets the latest version, or Cache-Control: private, max-age=600 if you want to cache user-specific content for a short period in their browser.
The browser’s own heuristics are a fascinating, often hidden, layer. If a browser detects that a user is repeatedly requesting a resource very quickly, or if it has a strong internal signal that the user is actively trying to refresh (e.g., hitting the refresh button), it might bypass even Cache-Control: max-age rules and perform a full revalidation or even a full download, regardless of what the server headers dictate. This behavior is not explicitly controllable via HTTP headers but is a deliberate design choice to improve user experience in certain interactive scenarios.
The next logical step in understanding web performance is exploring how CDNs leverage and extend these HTTP caching mechanisms.