Strong ETag
An ETag with no W/ prefix: its value changes whenever the representation bytes change, so two responses carrying the same strong ETag are byte-for-byte identical. RFC 9110 calls it a strong validator, and it is what If-Range resumption and If-Match concurrency checks require.
Also known as strong validator, strong entity tag.
Full Explanation
A strong ETag is an ETag whose value changes whenever the representation data changes. So two responses that carry the same strong ETag are byte-for-byte identical. It is the value with no weakness indicator. ETag: "abc123" is strong. ETag: W/"abc123" is a weak ETag. RFC 9110 calls the concept a strong validator. It defines a strong validator as "representation metadata that changes value whenever a change occurs to the representation data that would be observable in the content of a 200 (OK) response to GET" (RFC 9110 section 8.8.1).
Three things it is not. It is not a uniqueness claim across resources. RFC 9110 states that "there is no implication of uniqueness across representations of different resources". So the same strong value on two URLs proves nothing about them. It is not a synonym for strong validator in the wider sense: a Last-Modified date can also qualify as a strong validator under the deduction rules in section 8.8.2.2. And it is not a freshness rule. It says nothing about how long a copy may be reused. That is Cache-Control's job. A strong ETag answers one question, and answers it exactly: are these two bodies the same bytes?
That exactness is what makes it load-bearing. RFC 9110 section 8.8.1 puts the division of labour plainly: "Strong validators are usable for all conditional requests, including cache validation, partial content ranges, and 'lost update' avoidance. Weak validators are only usable when the client does not require exact equality with previously obtained representation data, such as when validating a cache entry or limiting a web traversal to recent changes."
How it works
An entity tag is an opaque quoted string, optionally prefixed by a weakness indicator. The grammar is opaque-tag = DQUOTE *etagc DQUOTE. So the double quotes are part of the field value, not decoration. A tag "can be either a weak or strong validator, with strong being the default". If the way an origin server generates a tag does not satisfy all the characteristics of a strong validator, the RFC requires that server to mark the tag as weak. It does this by prefixing the opaque value with the case-sensitive W/ (RFC 9110 section 8.8.3). Strength is therefore a promise the origin makes about its own generator. It is not something a client can test.
Generators that can keep the promise are few. RFC 9110 names two. The first is strict revision control: "wherein each change to a representation always results in a unique node name and revision identifier being assigned before the representation is made accessible to GET". The second is "a collision-resistant hash function applied to the representation data". That hash function is "also sufficient if the data is available prior to the response header fields being sent". Anything derived from a coarse clock is not sufficient on its own.
Comparison is where strength cashes out. HTTP defines two comparison functions (section 8.8.3.2). Under strong comparison, "two entity tags are equivalent if both are not weak and their opaque-tags match character-by-character". Under weak comparison, they are equivalent "if their opaque-tags match character-by-character, regardless of either or both being tagged as 'weak'". So "1" and "1" match under both functions. W/"1" and "1" match only weakly. W/"1" and W/"1" never match strongly.
Each conditional header field picks one of those functions, and that choice is the whole story:
- If-None-Match: the client stores the ETag from the first response and sends it back on the next request for that URL. A recipient "MUST use the weak comparison function when comparing entity tags for If-None-Match" (section 13.1.2). That is because weak tags are good enough for cache validation. When the condition evaluates false, the server answers 304 (Not Modified). A 304 "is terminated by the end of the header section; it cannot contain content or trailers" (section 15.4.5). No body crosses the wire.
- If-Match: an origin server "MUST use the strong comparison function when comparing entity tags for If-Match" (section 13.1.1). So a weak tag can never satisfy it. This is the header used with PUT, POST and DELETE to prevent the lost-update problem. It answers 412 (Precondition Failed) when the representation moved underneath the client.
- If-Range: "A client MUST NOT generate an If-Range header field containing an entity tag that is marked as weak" (section 13.1.5). The server evaluates the tag it does receive with the strong comparison function.
Here is the whole exchange on the wire:
// First request
GET /static/app.js HTTP/1.1
Host: cdn.example.com
// First response
HTTP/1.1 200 OK
ETag: "a1b2c3d4e5f6"
Content-Length: 48293
Cache-Control: max-age=3600
// Subsequent request, once the stored copy is stale (browser sends stored ETag)
GET /static/app.js HTTP/1.1
Host: cdn.example.com
If-None-Match: "a1b2c3d4e5f6"
// Response when unchanged. Cache-Control repeats, because RFC 9110 section
// 15.4.5 makes it a MUST on any 304 that a 200 would have carried it on
HTTP/1.1 304 Not Modified
ETag: "a1b2c3d4e5f6"
Cache-Control: max-age=3600
One myth is worth killing here. A strong ETag is not required to serve a byte range request. A bare Range request carries no validator at all, and a willing server answers 206 (Partial Content). If-Range merely "can be used as a precondition to applying the Range header field" (section 14.2). What genuinely needs a strong validator is safe resumption and reassembly. Partial responses "can only be safely combined if they all have in common the same strong validator" (section 15.3.7.3). And a cache may combine stored ranges only "if they all share the same strong validator" (RFC 9111 section 3.4). Without one, an interrupted download starts over.
Why it matters for a CDN
- Bodyless revalidation on both legs. When a stale edge object carries a validator, the edge revalidates with If-None-Match instead of re-fetching. So the hop between edge and origin carries headers only. The same exchange runs on the last mile, when a browser revalidates against the edge. What is saved is the body. For a large object, the body is essentially the whole cost.
- Choosing which stored variant to freshen. This is the reason strength matters specifically to a cache. When a cache receives a 304, "If the new response contains one or more 'strong validators' ... then each of those strong validators identifies a selected representation for update". And "If none of the initial set contains at least one of the same strong validators, then the cache MUST NOT use the new response to update any stored responses" (RFC 9111 section 4.3.4). An edge holding several negotiated variants of one URL uses strong ETags to freshen exactly the right one. With only weak validators, it can do no better than freshen the most recent match.
- Resumable large-object delivery. Video, installers and OS images are fetched in pieces by clients on unreliable networks. Pieces may only be stitched together when they share a strong validator. That is the difference between a resumed download and a restarted one. On a 4 GB object, it is the difference a user notices.
- Optimistic concurrency for APIs behind the edge. If-Match is strong-comparison only. So an API fronted by a CDN needs genuine strong ETags for write-conflict detection to work at all. A weak tag fails the precondition every time.
What CDNs do
Handling differs per provider, so check it against current documentation. That matters because most CDNs weaken or drop a strong tag as soon as they touch the bytes:
- Cloudflare treats strong ETag validation as opt-in: "Use Cache Rules to enable strong ETag headers", through the Respect Strong ETags setting. Even with it enabled, Cloudflare converts the tag to a weak one whenever it has to decompress or recompress a body to match the visitor's accept-encoding. For example, an origin tag of "foobar" comes back as W/"foobar" when the origin sent gzip and the visitor accepts only Brotli. Two further details bite in practice. An incorrectly formatted, unquoted value is removed outright, rather than weakened. And "If a resource is cacheable and there is a cache miss, Cloudflare does not send ETag headers to the origin server", because the full body is needed to fill the cache.
- Akamai exposes this as the Validate Entity Tag (ETag) behaviour: "When this behavior is enabled, strong ETags are always supported. You can optionally choose to support weak ETags, which allows the edge to perform matching on W/-prefixed ETag values". With weak support off, a weak ETag or weak If-None-Match value makes the edge return a full 200 rather than a 304. A separate option, Allow non-strict Strong ETags, permits matching on unquoted values. The documentation calls these values "technically malformed". But it also notes that they "appear frequently in real-world origin responses".
- Fastly revalidates conditionally whenever a validator is present: "If the 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". Fastly documents the inverse control too. Unsetting ETag and Last-Modified in vcl_fetch disables revalidation entirely.
Watch out for
- Transforming bytes invalidates the tag, and good servers act on it. nginx makes this explicit in code. Its gzip filter calls ngx_http_weak_etag(), which rewrites an existing quoted tag as W/"...". The ssi and sub filters either weaken or clear the tag (ngx_http_core_module.c). A response that was resumable before compression is not resumable after it.
- Every content coding needs its own tag. RFC 9110 warns that "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" (section 8.8.3.3). Section 8.8.1 is blunter: "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". Reusing one tag across Content-Encoding variants is a bug, not an economy.
- nginx's automatic tag is only nominally strong. The etag directive is on by default. With it on, nginx generates an ETag for static resources automatically. The value is the last-modified time and the content length in hex, formatted as "%xT-%xO". It carries no W/ prefix, so it is syntactically strong. But its inputs are a one-second timestamp and a byte count. RFC 9110 section 8.8.1 names that very pattern as a weak-validator basis. A representation's modification time, "if defined with only one-second resolution, might be a weak validator if it is possible for the representation to be modified twice during a single second". Two edits inside one second that happen to produce the same length collide. A build-time content hash has no such failure mode.
- A 304 is not proof of byte equality. If-None-Match is always evaluated with weak comparison. So a 304 may have been triggered by a weak match. Read 304 as "your cached copy is still usable", never as "your cached copy is byte-identical".
- Unquoted values are malformed. Origins emit them anyway, and each CDN salvages them differently. Akamai offers an opt-in to match on them, Cloudflare deletes the header. Neither is behaviour to build on.
- A strong tag saves nothing on a cache fill. A miss needs the whole body regardless of validators. Strong ETags pay off on revalidation and on resumption, not on first fetch. So they are no substitute for a sensible freshness lifetime.
Best practice
- Generate the tag from a collision-resistant hash of the response body, or from a build revision identifier, quoted, with no W/ prefix. Those are the two generators RFC 9110 endorses.
- Emit a distinct strong tag per content coding. Send Vary: Accept-Encoding. Then the compressed and uncompressed representations of one URL never share a validator.
- Keep tags strong on large resumable objects. A silent downgrade to W/ on a video or installer removes If-Range resumption and range reassembly. Nothing in the response announces it.
- Prefer ETag over Last-Modified when a resource can change more than once per second: an entity tag "can be more reliable for validation than a modification date ... where the one-second resolution of HTTP-date values is not sufficient".
- Verify at the edge, not at the origin. Request the same URL twice: once with Accept-Encoding: gzip, and once without. Compare the ETag on each response. A W/ value, or a missing value, means the resumption and variant-freshening guarantees are gone.
- Turn the provider's strong-ETag setting on deliberately, and read what it disables. Enabling Cloudflare's Respect Strong ETags, for instance, automatically disables Rocket Loader, Email Obfuscation and Automatic HTTPS Rewrites. That is because those features rewrite bodies.
- Never read a strong ETag as cross-URL identity. It vouches for one representation of one resource and for nothing else.
Interactive Animation
Examples
Nginx makes strong ETags for static files by itself. The value comes from the file time and size.
# Check ETag behavior with curl
curl -I https://cdn.example.com/style.css
# ETag: "6523af7c-1a4f"
# Conditional request
curl -H 'If-None-Match: "6523af7c-1a4f"' -I https://cdn.example.com/style.css
# HTTP/1.1 304 Not Modified
Frequently Asked Questions
An ETag with no W/ prefix: its value changes whenever the representation bytes change, so two responses carrying the same strong ETag are byte-for-byte identical. RFC 9110 calls it a strong validator, and it is what If-Range resumption and If-Match concurrency checks require.
Nginx makes strong ETags for static files by itself. The value comes from the file time and size.
# Check ETag behavior with curl
curl -I https://cdn.example.com/style.css
# ETag: "6523af7c-1a4f"
# Conditional request
curl -H 'If-None-Match: "6523af7c-1a4f"' -I https://cdn.example.com/style.css
# HTTP/1.1 304 Not Modified
Yes. Strong ETag is also known as strong validator, strong entity tag. An ETag with no W/ prefix: its value changes whenever the representation bytes change, so two responses carrying the same strong ETag are byte-for-byte identical. RFC 9110 calls it a strong validator, and it is what If-Range resumption and If-Match concurrency checks require.