ETags are a surprisingly effective way to shave off bandwidth by letting the server tell the browser, "Hey, you’ve already got the latest version of this file, so don’t bother sending it again."
Let’s see this in action. Imagine a browser requesting an image:
GET /images/logo.png HTTP/1.1
Host: example.com
If-None-Match: "abcdef12345"
HTTP/1.1 304 Not Modified
Content-Length: 0
ETag: "abcdef12345"
Notice the If-None-Match header from the browser. The browser is saying, "I have a version of logo.png that has this ETag. Is it still the same?" The server checks its logo.png and sees that its current ETag is also "abcdef12345". Because they match, it sends back a 304 Not Modified response. This response has a Content-Length of 0 because it’s not sending the actual image data, just a confirmation that the browser’s cached version is still good. This saves a ton of data transfer.
The core problem ETags solve is the inefficiency of re-downloading resources that haven’t changed. Without them, every request for a static asset like an image, CSS file, or JavaScript file would involve transferring the entire file, even if the server’s copy is identical to what the browser already has cached. This wastes network bandwidth, increases server load (because the server still has to process the request and send the file), and slows down page load times for the user.
ETags work by providing a unique identifier for a specific version of a resource. When a server sends a resource to the browser, it can include an ETag header in the response. This ETag is typically a hash of the file’s content or a version number. The browser then stores this ETag along with the cached resource. The next time the browser needs that resource, it includes the stored ETag in an If-None-Match header in its request. The server compares the ETag provided by the browser with the ETag of its current version of the resource. If they match, the server knows the browser has the latest version and sends a 304 Not Modified response. If they don’t match, the server sends the new version of the resource with a 200 OK status and a new ETag header.
The exact way an ETag is generated is up to the web server. For static files, it’s often derived from the file’s last modification timestamp and size, or a cryptographic hash (like MD5 or SHA-1) of the file content. For dynamically generated content, it might be based on a version number embedded in the content or a hash of the computed output. The key is that the ETag must change if and only if the resource content changes.
Consider a web server like Nginx. For static files, Nginx can automatically generate ETags based on the file’s inode, modification time, and size. You don’t need to do much to enable it. If you’re using Apache, the mod_etag module handles this by default, often using a combination of file size, modification time, and inode number.
The If-None-Match header is the client’s (browser’s) way of saying, "I have this ETag, is it still valid?" The server then uses the ETag header in its response to confirm or deny. If the ETag matches, the server sends back a 304 Not Modified status code, an empty response body, and crucially, it doesn’t send the Content-Type or Content-Length headers because there’s no content to send. If the ETag doesn’t match, the server sends a 200 OK status code, the full resource, and a new ETag header.
A common misconception is that ETags are only for static files. While they are most commonly seen and easily implemented for static assets, they are incredibly powerful for dynamic content as well. For example, if a user requests their profile page, the server can generate an ETag based on the last time their profile data was updated. If the user requests the same page again shortly after, and their profile hasn’t changed, the server can respond with 304 Not Modified, saving the cost of re-rendering the entire page. This requires application-level logic to generate and manage the ETags appropriately.
It’s also important to understand the difference between If-None-Match and If-Match. If-None-Match is used for checking if a resource is different from what the client has (leading to a 304). If-Match is used for conditional updates, where the client is sending data and wants to ensure it’s modifying the exact version they last saw (leading to a 412 Precondition Failed if the ETag doesn’t match).
When you enable ETags, you should also consider the Cache-Control and Expires headers. These headers tell the browser how long it’s allowed to cache a resource before it should even bother checking with the server using the ETag. A common strategy is to set a long Cache-Control: max-age=... for static assets that rarely change, and then rely on the ETag validation for subsequent checks.
If you disable ETags on your server, the browser will always have to re-download the full resource, even if it hasn’t changed, leading to increased bandwidth usage and slower load times.
The next logical step after mastering ETags for bandwidth saving is understanding how to manage cache invalidation when resources do change.