ETag
An HTTP response header carrying an entity tag: an opaque validator identifying one version of one representation. A client or CDN sends it back in If-None-Match, and a match returns 304 Not Modified instead of the body. Strong tags mean byte-identical, weak tags (W/) only equivalent.
Also known as entity tag.
Full Explanation
ETag is the HTTP response header field that carries an entity tag. An entity tag is an opaque validator. It identifies one specific version of one representation of a resource, for example ETag: "abc123". RFC 9110 defines it as "an opaque validator for differentiating between multiple representations of the same resource" (RFC 9110 section 8.8.3). Representations can differ for two reasons: the resource changed over time, or content negotiation produced several valid representations at once. Both reasons can also apply together. An ETag exists to answer one question cheaply: is the copy you already hold still current? A client, or a CDN edge acting as a cache, sends the stored tag back in If-None-Match. A server that finds a match answers 304 Not Modified with headers only. It does not resend the body.
An ETag is not a freshness mechanism. It is not a checksum the recipient may interpret, either. It carries no lifetime. How long a copy may be reused before anyone asks again is Cache-Control's job. The value is opaque: "there is no need for the client to be aware of how each entity tag is constructed" (RFC 9110 section 8.8.3.1). So nothing outside the origin may parse it or compare it against a hash of its own. An ETag is also not HTTP's only validator. Last-Modified is the other one. A Last-Modified time used as a validator "is implicitly weak unless it is possible to deduce that it is strong" (RFC 9110 section 8.8.2.2). That is why the entity tag is the precise one.
Every tag is one of two kinds. The difference decides what it may be used for. A strong tag changes value "whenever a change occurs to the representation data that would be observable in the content of a 200 (OK) response to GET". Strong tags are "usable for all conditional requests, including cache validation, partial content ranges, and "lost update" avoidance". A weak tag is marked with the case-sensitive prefix W/. It may stay unchanged across edits the origin considers equivalent. So it is "only usable when the client does not require exact equality with previously obtained representation data" (RFC 9110 section 8.8.1). In practice, that means cache validation.
How it works
- Generate. The origin computes a value for the version of the representation it is about to send. It returns the value in the ETag field as a double-quoted opaque string, optionally prefixed with W/. The derivation is the origin's choice: a revision identifier, "a collision-resistant hash of representation content, a combination of various file attributes, or a modification timestamp that has sub-second resolution". But if the way the tag is generated "does not satisfy all of the characteristics of a strong validator", the origin server MUST mark it weak. It does this by prefixing the opaque value with W/ (RFC 9110 section 8.8.3).
- Store. The requester stores the tag alongside the response it belongs to. The requester may be a browser, a proxy, or a CDN edge server.
- Recheck. On a later request for the same resource, the stored tag goes back in If-None-Match. A recipient MUST use the weak comparison function for If-None-Match. The reason: "since weak entity tags can be used for cache validation even if there have been changes to the representation data" (RFC 9110 section 13.1.2). So
W/"abc123"and"abc123"match each other here. - Decide. A listed tag that matches makes the condition false. The server MUST NOT perform the method. It MUST answer 304 Not Modified for GET or HEAD, or 412 Precondition Failed for any other method. A 304 "is terminated by the end of the header section; it cannot contain content or trailers" (RFC 9110 section 15.4.5). It MUST repeat the fields a 200 would have carried that the recipient needs to update its stored copy: Content-Location, Date, ETag and Vary, plus Cache-Control and Expires. No match means an ordinary 200 with the new body and a new tag.
Two comparison functions decide equality. Which one applies is fixed by the header field, not by preference. Strong comparison holds only when "both are not weak and their opaque-tags match character-by-character" (RFC 9110 section 8.8.3.2). Weak comparison holds when the opaque values match, regardless of either or both being marked weak. If-None-Match, and therefore cache validation, uses weak comparison. If-Match and If-Range use strong comparison. That is exactly why a weak tag is worthless to them.
Validation is not the only use. The same tag drives optimistic concurrency control. A client can send If-Match carrying the tag it last read. This tells the origin to apply the PUT, POST or DELETE only if nobody else changed the representation meanwhile. This is HTTP's answer to what RFC 9110 calls the "lost update" problem. An origin server MUST use strong comparison for If-Match. It MUST NOT perform the method when the condition is false. It MAY report the failure with 412 Precondition Failed (RFC 9110 section 13.1.1). If-None-Match: * is the create-only variant: write this only if no representation exists yet.
Why it matters for a CDN
A CDN is a shared cache sitting in front of the origin. Entity tags are what let it keep serving without dragging bodies across the network again. Three rules in the caching spec shape edge behaviour:
- Revalidating upstream. When generating a conditional request, a cache "MUST send the relevant entity tags (using If-Match, If-None-Match, or If-Range) if the entity tags were provided in the stored response(s) being validated" (RFC 9111 section 4.3.1). It SHOULD also add If-Modified-Since when the stored response has a Last-Modified value. A stale but unchanged 4 GB video therefore costs one header exchange to keep, not a re-fetch.
- Answering downstream. Browsers and child caches send their own If-None-Match. A cache could satisfy the request by reusing a stored 200 or 206. Such a cache "SHOULD evaluate any applicable conditional header field preconditions received in that request with respect to the corresponding validators contained within the stored response" (RFC 9111 section 4.3.2). That is how an edge returns a 304 without contacting the origin at all.
- Freshening the right entry. If a 304 carries strong validators, only stored responses holding the same strong validator are updated. If none of them holds one of those strong validators, "the cache MUST NOT use the new response to update any stored responses" (RFC 9111 section 4.3.4). Unstable tags do not merely cause misses. They also leave revalidation unable to freshen the copy the edge already has.
Ranges are the other CDN-shaped consequence. Video seeking and resumable downloads are byte range requests. A client that wants to continue a partial copy sends If-Range with the validator it holds. An unchanged representation then yields the missing bytes, and a changed one yields the whole thing. "A client MUST NOT generate an If-Range header field containing an entity tag that is marked as weak" (RFC 9110 section 13.1.5). The tag is compared with the strong comparison function. Serving ranges needs no validator at all. Resuming safely is what needs a strong one.
What CDNs do
Behaviour differs by vendor. Some strong-validator handling is opt-in. Any CDN that changes the bytes it serves must change or drop the tag that described them.
- Cloudflare "supports both strong and weak ETags configured at your origin web server". Strong handling is opt-in. Enabling Respect Strong ETags in a cache rule makes Cloudflare "use strong ETag header validation to ensure that resources in the Cloudflare cache and on the origin server are byte-for-byte identical". Enabling it also automatically disables Rocket Loader, Email Obfuscation and Automatic HTTPS Rewrites. Even with it enabled, the strong tag survives only while Cloudflare can pass the origin's content coding through to a visitor who accepts it. Where it must decompress or transcode, for example when the origin sent Zstandard and the visitor accepts Brotli, the documented action is a weak tag,
W/"foobar". With the feature disabled, the everyday case of re-compressing a GZIP origin response as Brotli demotes the tag as well. On a cache miss for a cacheable resource, Cloudflare "does not send ETag headers to the origin server". That is because it "requires the full response body to fill its cache" (Cloudflare: Using ETag headers). - Fastly revalidates conditionally whenever it can. If a stale object "has a validator (an ETag or Last-Modified header), the readthrough cache interface performs a conditional revalidation by sending the headers If-None-Match, If-Modified-Since, or both". A 304 "will cause the lifetime of the existing object to be extended" and "will reset the object's Age". Without a validator, the revalidation is unconditional: a plain re-fetch of the whole body. A successful revalidation also changes nothing else about the stored object. "No other aspects of the existing cached response will be modified" (Fastly: Lifetime and revalidation). So later client requests get the headers from the original backend response rather than those on the 304.
- Akamai gates edge-side validation behind the optional Validate Entity Tag behaviour. With it enabled, when a request arrives carrying If-None-Match, "the edge server compares it to the ETag value stored with the cached object". On a match, it returns "a lightweight 304 Not Modified response with no body". On a mismatch, it returns either the full object from cache or a fresh copy from the origin. "strong ETags are always supported". Weak matching is a separate option, and when it is off a weak value "returns a full 200 response rather than a 304". A further option allows matching unquoted strong tags. These are "technically malformed" but "appear frequently in real-world origin responses" (Akamai: Validate Entity Tag (ETag)).
Watch out for
- Every origin server must produce the same tag for the same bytes. A validator is only useful if whichever server answers next computes the value the requester stored. Apache is the classic trap. FileETag defaults to MTime Size. The directive documentation records that the default was INode MTime Size in 2.3.14 and earlier. The INode keyword means "The file's i-node number will be included in the calculation" (Apache: FileETag). Inode numbers "are guaranteed to be unique only within a filesystem" (inode(7)). So two servers holding identical bytes emit different tags. The modern default is no safer in a fleet: a deploy that leaves different modification times per machine changes MTime, and so the tag. nginx has the same shape of problem. Automatic generation for static resources is on by default ("Default: etag on", nginx: etag). The value is formatted from the last-modified time and the content length rather than the content (ngx_http_set_etag). Divergent tags turn every conditional request into a full re-download. That is a permanent stream of cache misses that the hit-ratio graph will not explain.
- Anything that rewrites the body has to weaken or drop the tag. Content codings "are a property of the representation data, so a strong entity tag for a content-encoded representation has to be distinct from the entity tag of an unencoded representation to prevent potential conflicts during cache updates and range requests" (RFC 9110 section 8.8.3.3). And "if the origin server sends the same validator for a representation with a gzip content coding applied as it does for a representation with no content coding, then that validator is weak" (RFC 9110 section 8.8.1). Implementations obey this in ways that surprise people. nginx's gzip filter converts the tag to weak and clears Accept-Ranges when it compresses on the fly (ngx_http_gzip_filter_module). Cloudflare warns that with weak ETags "it is necessary to disable certain features such as Email Obfuscation and Automatic HTTPS Rewrites to prevent Cloudflare from removing the ETag headers set by your origin web server".
- A weak tag cannot resume a partial transfer. If-Range rejects weak tags by design. The reason: a weak tag does not promise byte-for-byte identity, so the resumed bytes would not fit the bytes already held. This is a limit on conditional range requests, not on ranges as such. A plain Range request needs no validator. But anything that stitches a new response onto an old partial one needs a strong one.
- Strong tags must be double-quoted. Unquoted values are malformed yet common. Cloudflare states that with an incorrect format it "will remove the ETag header instead of converting it to a weak ETag". Akamai offers an opt-in "Allow non-strict Strong ETags" instead. An origin emitting bare tags may therefore lose validation entirely, silently, at one CDN, and keep it at another.
- It is not expiry. An ETag never says how long a copy stays fresh. Cache-Control and Expires decide when a cache must ask again. The entity tag only decides what the answer is. A response marked no-cache "MUST NOT be used to satisfy any other request without forwarding it for validation" (RFC 9111 section 5.2.2.4). So a perfect tag paired with no-cache still costs a round trip per request: correct, cheap in bytes, and not fast.
Best practice
- Send one wherever you can. "An origin server SHOULD send an ETag for any selected representation for which detection of changes can be reasonably and consistently determined" (RFC 9110 section 8.8.3.1). That is because its use in conditional requests and freshness evaluation "can substantially reduce unnecessary transfers and significantly improve service availability, scalability, and reliability".
- Derive it from the content, not from the filesystem. "A collision-resistant hash function applied to the representation data is also sufficient if the data is available prior to the response header fields being sent" (RFC 9110 section 8.8.1). Alternatively, use a build or revision identifier that every server in the fleet shares. On Apache, FileETag Digest calculates the tag "by taking the digest over the file". That is stable across machines where MTime and INode are not. Tag stability across your origin fleet is the single biggest determinant of whether CDN revalidation ever succeeds.
- Prefer strong, and mark weak honestly. Use a strong tag for cache validation, range resumption and If-Match writes. Where you cannot guarantee byte-for-byte identity, emit W/ rather than an unstable strong tag. A weak tag still yields 304s under If-None-Match. A strong tag that lies, though, breaks range resumption and concurrency control.
- One tag per representation. Never share a strong tag across bodies that differ in bytes. Give each Content-Encoding and each negotiated variant its own value. Keep Vary honest about what you negotiated on. Check that your CDN's cache key and Vary agree, or the edge will validate one variant against another's tag.
- Pair it with Cache-Control, and send both validators. Cache-Control decides when a cache asks. The tag decides what it hears. If a request carries both, a recipient "MUST ignore If-Modified-Since if the request contains an If-None-Match header field". The tag is "a more accurate replacement" (RFC 9110 section 13.1.3). So sending Last-Modified as well costs nothing and helps older intermediaries.
- Verify what your provider actually does, per property. Cloudflare needs Respect Strong ETags enabled, and it still weakens tags when it changes content coding. Akamai needs Validate Entity Tag enabled, plus its weak-matching option if your origin emits W/ tags. Fastly revalidates conditionally with whatever validator it has. Test it: curl the edge and note the tag. Change one byte at the origin. Confirm the next conditional request gets 200 with a new tag rather than a 304. Also confirm a compressed variant still validates.
Interactive Animation
Examples
# Server response with ETag
HTTP/1.1 200 OK
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Content-Length: 12345
# Client conditional request
GET /style.css HTTP/1.1
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
# Server: content unchanged
HTTP/1.1 304 Not Modified
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
# No body sent - saves bandwidth
# Nginx: ETags are on by default
# To disable (rare): etag off;
# Generate consistent ETags from content hash
etag on; # Uses last-modified + content-length
Frequently Asked Questions
An HTTP response header carrying an entity tag: an opaque validator identifying one version of one representation. A client or CDN sends it back in If-None-Match, and a match returns 304 Not Modified instead of the body. Strong tags mean byte-identical, weak tags (W/) only equivalent.
# Server response with ETag
HTTP/1.1 200 OK
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Content-Length: 12345
# Client conditional request
GET /style.css HTTP/1.1
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
# Server: content unchanged
HTTP/1.1 304 Not Modified
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
# No body sent - saves bandwidth
# Nginx: ETags are on by default
# To disable (rare): etag off;
# Generate consistent ETags from content hash
etag on; # Uses last-modified + content-length
Yes. ETag is also known as entity tag. An HTTP response header carrying an entity tag: an opaque validator identifying one version of one representation. A client or CDN sends it back in If-None-Match, and a match returns 304 Not Modified instead of the body. Strong tags mean byte-identical, weak tags (W/) only equivalent.
Related CDN concepts include:
- Age Header — The Age response header is a cache's estimate, in seconds, of how long ago the …
- Strong ETag — An ETag with no W/ prefix: its value changes whenever the representation bytes change, so …
- Weak ETag — A weak ETag is an ETag whose value carries the case-sensitive W/ marker, as in …
- Cache-Control — Cache-Control is the HTTP header field, defined in RFC 9111, that carries caching directives to …