ETag Header Explained: How Browsers Revalidate Cached Files
An ETag header is the small, quoted string a server attaches to a response so that later on, the browser can ask "has this changed since I last saw it?" instead of downloading the whole file again. If you ran a page through httpcheck.tools and saw a no_validator or weak_etag flag in the caching section, you're already in the right place to understand what that means and whether it's worth fixing.
This is one of those problems that looks like it belongs to a single layer but actually touches three: the browser doing the caching, the origin server generating the header, and (increasingly often) a CDN sitting between them rewriting it. Get the mental model straight first, and the fix at each layer stops feeling like guesswork.
What Is an ETag Header?
An ETag (short for entity tag) is a response header the server sends alongside a file. It looks something like this.
ETag: "5d8c72a5edda3"
That string is a fingerprint of the response body at the moment it was served. Change the file, the fingerprint changes. Serve the exact same bytes again, the fingerprint stays put. The browser stores that fingerprint next to the cached copy of the file, and the next time it needs the resource, it can hand the fingerprint back to the server as a question: is this still current?
RFC 9110, the IETF standard that governs HTTP semantics, defines the ETag as an opaque validator.
Opaque is the key word. Neither the browser nor anyone reading the header is meant to decode meaning from the string itself. It doesn't have to be a hash, a version number, or anything human-readable. It just has to change reliably when the content changes, and stay stable when it doesn't.
How the ETag and If-None-Match handshake works
This is the part that actually answers how does ETag work, and it's a three-step exchange between two systems: the browser (or any HTTP client, including Googlebot) and the origin server.
Step one. The browser requests a file for the first time. The server responds with a normal 200 OK and includes an ETag header. The browser caches the response body and remembers the ETag value alongside it.
Step two. Later, the browser needs that file again, perhaps because the cached copy has gone stale according to its Cache-Control directives. Rather than fetching it blind, the browser sends a conditional request with an If-None-Match header carrying the ETag it stored.
GET /styles/main.css HTTP/1.1
If-None-Match: "5d8c72a5edda3"
Step three. The server compares the ETag it would generate for the current version of the file against the one the browser sent. If they match, nothing has changed from the browser's point of view, so the server responds with a 304 Not Modified and no response body at all. If they don't match, the server sends the full response with a 200 and a new ETag.
HTTP/1.1 304 Not Modified
ETag: "5d8c72a5edda3"
Cache-Control: max-age=3600
RFC 9110 section 13.1.2 spells out exactly what that 304 response is allowed to repeat: Cache-Control, Content-Location, Date, ETag, Expires, and Vary, but never the body. That's the entire point. A 304 costs a round trip and a handful of header bytes instead of the full page weight. For a product page with a two-megabyte hero image or a news site publishing dozens of assets per article, that difference adds up across every repeat visitor and every crawler pass.
Weak vs strong ETags: the W/ prefix
Not every ETag promises the same level of precision. You'll sometimes see one prefixed with W/, like this.
ETag: W/"5d8c72a5edda3"
That prefix marks it as a weak validator. Per RFC 9110 section 8.8.1, a strong validator has to change any time the underlying data changes in any way a user would notice. A weak validator only has to stay semantically equivalent, meaning it can stay the same across changes the server considers insignificant, like whitespace normalization or a timestamp buried in a comment. The standard's own summary is blunt about the relationship: a strong validator is equivalent to a weak validator, but not the other way around.
Weak ETags are cheap to generate, which is why a lot of dynamic content (a blog's article page rendered from a template, for instance) uses them. The tradeoff, as MDN's ETag reference notes, is that weak ETags can't validate cached byte-range responses. If your site serves large downloads or video with range requests, a strong ETag is the one doing real work there. For an ordinary HTML page or a stylesheet, weak is usually fine, and the tool's weak_etag flag is informational rather than an emergency.
ETag vs Last-Modified: which one wins?
ETag and Last-Modified solve the same problem through different mechanisms, and understanding the etag vs last-modified tradeoff clears up a lot of confusion about which one a given server should send.
Last-Modified is a timestamp: the last time the file changed, down to the second. The browser's conditional counterpart is If-Modified-Since. It's simple, human-readable, and cheap for the server to produce, since most filesystems already track modification time.
Its weakness is precision. A file rewritten twice within the same second produces an identical Last-Modified value even though the content differs, and some content management systems touch a file's timestamp without changing a single byte of output, which forces an unnecessary full re-download.
ETag doesn't have that blind spot, because it fingerprints the actual bytes rather than a clock reading. That's the whole reason both mechanisms tend to appear on the same response: Last-Modified as a simple, widely supported fallback, ETag as the more exact check.
What changed the stakes here is a piece of guidance Google published in its "Crawling December" series. According to Google Search Central's December 2024 post on HTTP caching, Googlebot's crawling infrastructure supports heuristic caching through both ETag/If-None-Match and Last-Modified/If-Modified-Since, and when a response carries both, Google's crawlers use the ETag value first, in line with the HTTP standard. If you're running a large site and crawl efficiency matters to you, that's a direct, current reason to make sure ETag isn't the one missing.
Where ETags quietly break
Here's the failure mode most explainers skip, and it's the reason a site can have ETags present on every response and still get zero benefit from them.
The Apache multi-server mismatch
Older Apache configurations generated ETags from three pieces of file metadata: inode number, modification time, and file size, controlled by the FileETag directive. Inode numbers are assigned by the filesystem, and on a setup with two or more web servers behind a load balancer (a common pattern for anything beyond a small shop or blog), the exact same file on server A and server B can have two different inode numbers. Same content, different ETag, every single time. The browser sends its If-None-Match, the server it happens to land on compares against a different value, nothing matches, and the client re-downloads a file that hasn't actually changed.
Apache's own documentation for the FileETag directive reflects the fix: the modern default configuration drops inode from the ETag calculation, generating it from modification time and size instead. If your site runs on multiple origin servers, it's worth confirming which components your config actually uses, since inherited or copied configs sometimes still carry the old setting forward.
The mod_deflate compression suffix
The second failure lives at the intersection of compression and validation. Apache's mod_deflate module appends a -gzip suffix to the ETag of a compressed response, so a plain "5d8c72a5edda3" becomes "5d8c72a5edda3-gzip". The trouble shows up on the comparison step: some Apache configurations don't strip that suffix when checking an incoming If-None-Match value against the stored ETag. A browser can send back the literal ETag the server gave it and still get a fresh 200 instead of a 304, because the string it's holding no longer matches the suffixed one on the server.
If you've ever debugged a case where "I sent the exact ETag back and it still re-downloaded," this is very likely why.
CDN transcoding
The third layer is the one that's easy to forget about entirely: whatever sits in front of your origin. Cloudflare's own documentation explains that it preserves a strong ETag only when the compression it serves to a given visitor matches what the origin sent; otherwise, because the response gets decompressed and recompressed on the way through, Cloudflare converts a strong ETag to a weak one. Certain content-rewriting features can strip the ETag altogether if it isn't quoted correctly. So a perfectly configured origin can still show up as weak or missing on the edge, and the fix in that case lives in the CDN dashboard, not in your .htaccess file.
Should you remove ETags on Apache?
You'll still find hosting-optimization guides from the PageSpeed-tutorial era recommending you strip ETags entirely rather than bother configuring them. That advice wasn't wrong when it was written. It was a real fix for a real 2006-era problem: inode-based ETags breaking revalidation across multi-server Apache setups, which is exactly the first failure mode above. Turning ETags off entirely was a blunt but effective workaround at the time.
Two things have moved on since then. Apache 2.4 changed its own default away from inode-based generation, closing the original hole for anyone running a current config. And Google now explicitly documents using ETag ahead of Last-Modified when both are present, which means disabling ETags today doesn't dodge a bug, it forfeits a crawl-efficiency signal Google is actively using.
The old advice fixed one problem by discarding a mechanism that current guidance rewards you for keeping. Removing ETags is the wrong layer to solve a load-balancer mismatch; fixing the FileETag directive is the right one.
There's a related trap worth naming separately. Some WordPress caching plugins add Cache-Control: no-store, which tells the browser never to keep a cached copy at all, meaning it never sends If-None-Match again regardless of what the ETag says. Site owners who see this often conclude "ETags don't work," when the actual cause is a caching directive overriding the validator's chance to do anything. That's a caching-policy decision, a different layer from the validator itself, and worth checking separately before you touch anything ETag-related.
Reading ETag issues in an httpcheck.tools report
When you run a URL through the httpcheck.tools report, the caching section checks whether a validator is present at all and what shape it takes. A no_validator flag means the response carries neither an ETag nor a Last-Modified header, so there's nothing for a repeat visitor's browser (or a crawler) to revalidate against; every fetch is a full download regardless of whether the content changed. A weak_etag flag just means the header uses the W/ prefix, which, as covered above, is a legitimate choice for most HTML and template-rendered pages and only becomes a real limitation if you're serving byte-range content like downloads or video.
If you want the full picture of what each header in the response is doing, not just ETag, the site's header-by-header guide walks through the rest, and the status code reference covers the 304 alongside the other codes you'll see in a caching chain.
Once you've confirmed the actual problem, the fix for an Apache-hosted site is a one-line directive change.
FileETag MTime Size
Dropping INode from that list removes the exact mismatch that breaks revalidation across load-balanced servers, while keeping a validator your browsers and Google's crawlers can both use. If you're not comfortable hand-editing your .htaccess file, htaccess.tools can generate the directive for you without the trial and error.
Frequently Asked Questions
What is an ETag?
An ETag is an HTTP response header containing a quoted, opaque string that fingerprints a specific version of a resource. The server generates it, the browser stores it alongside the cached copy of the file, and later requests can send it back as an If-None-Match value to ask the server whether the content has changed since it was last fetched.
How does an ETag work with If-None-Match?
The browser sends the ETag it previously received back to the server inside an If-None-Match header on a conditional request. The server compares that value against the ETag it would generate for the current version of the resource. A match means nothing has changed, so the server replies with a 304 Not Modified and skips resending the body; a mismatch means the server sends the full response with a fresh 200 and an updated ETag.
What is the difference between ETag and Last-Modified?
Last-Modified is a timestamp showing when a file last changed, which is simple but imprecise: two edits inside the same second look identical, and some systems update the timestamp without changing the actual output. ETag fingerprints the response content itself, so it catches changes Last-Modified can miss. Google's crawling infrastructure uses ETag ahead of Last-Modified when a response includes both, per its December 2024 crawler guidance, so sending both together, with ETag as the more precise one, is the standard practice.
Should I remove ETags on Apache?
Generally, no. The old advice to disable ETags addressed a real but dated problem: Apache generating ETags from inode numbers that differ across servers in a load-balanced setup, breaking every revalidation. Apache 2.4 already changed its default away from inode-based generation, and current Google guidance treats ETag as the preferred validator when both ETag and Last-Modified are present. Fixing the FileETag directive solves the original problem without giving up the crawl-efficiency benefit; removing ETags outright solves it by discarding a mechanism current guidance rewards you for keeping.