ETag header explained: format, 304 responses and the gzip W/ prefix (tested on nginx)
An ETag is a version label for a response. The browser sends it back as If-None-Match, and if nothing changed the server answers 304 with no body. We measured how nginx does it.
1. The round trip
$ curl -sI https://site/app.js
HTTP/1.1 200 OK
Last-Modified: Thu, 01 Oct 2026 12:08:29 GMT
ETag: "6abe4d3d-1900"
$ curl -sI -H 'If-None-Match: "6abe4d3d-1900"' https://site/app.js
HTTP/1.1 304 Not Modified
A different value ("nope") got a normal 200 with the full file. The 304 saves the body, not the request: the client still asks every time unless Cache-Control lets it reuse its copy without asking.
2. What nginx's ETag is made of
"6abe4d3d-1900" is two hex numbers: 6abe4d3d = the file's modification time (1790856509 = 12:08:29 UTC, the Last-Modified above) and 1900 = its size (6,400 bytes). It is not a hash of the content. Consequence, measured: after touch with the content unchanged, the ETag became "6abe4438-1900" and the old ETag got a full 200. Deploys that rewrite every file, or servers behind a load balancer with different file times, give clients a new ETag for every file even when nothing changed.
3. W/ = weak, and gzip adds it
Request to nginx with gzip on | ETag returned |
|---|---|
| No Accept-Encoding | "6abe4d3d-1900" |
Accept-Encoding: gzip | W/"6abe4d3d-1900" + Content-Encoding: gzip |
The compressed bytes differ from the file, so nginx marks the ETag weak ("same meaning, maybe not the same bytes"). Revalidation still worked: If-None-Match: W/"6abe4d3d-1900" got a 304, and so did the weak form sent against the uncompressed URL. If a scanner or CDN reports "weak ETag", gzip is the usual reason, and it is harmless for caching.
4. Turning ETags off
location /static/ {
etag off; # nginx: no ETag header; Last-Modified is still sent
}
With etag off nginx sent no ETag but kept Last-Modified, so browsers can still revalidate with If-Modified-Since. Security scanners sometimes flag ETags as leaking inode numbers: that applies to old Apache defaults (FileETag INode MTime Size), not to nginx, whose ETag holds only time and size as shown above.
Check any URL's ETag, Last-Modified and Cache-Control with curl -sI or the free HTTP headers tool.
Frequently asked questions
What does the ETag header do?
It labels a version of a response. The client sends it back in If-None-Match; if it still matches, the server replies 304 Not Modified with no body. nginx did exactly that in our test.
What does W/ mean in an ETag?
Weak: equivalent content, not necessarily identical bytes. nginx turned the strong ETag into W/"…" when it gzipped the file, and still answered 304 to it.
Why did my ETag change when the file didn't?
nginx builds it from modification time and size. A touch with unchanged content changed the ETag in our test.
ETag or Last-Modified?
Both work for revalidation. nginx sends both by default; with 'etag off' it still sent Last-Modified.
Written with AI assistance. Measured on 2026-10-01 with curl 8.14.1 against nginx 1.31.6 in a throwaway container serving one 6,400-byte JavaScript file, with gzip on and off and with 'etag off'. The Apache FileETag remark is from Apache's documentation, not this test. PlainHeaders is our extension; curl and the HTTP headers tool are free.