Weak ETag

Caching

A weak ETag is an ETag whose value carries the case-sensitive W/ marker, as in ETag: W/"abc123". It promises that two representations sharing the value are semantically equivalent, not byte-for-byte identical, so it can earn a 304 but cannot serve as a range or If-Match precondition.

Also known as weak entity tag.

14 min read Updated Aug 30, 2026

Full Explanation

A weak ETag is an ETag. Its value carries the case-sensitive weakness marker W/, as in ETag: W/"abc123". It tells any recipient that two representations sharing that value are semantically equivalent, meaning close enough to reuse. It does not promise the representations are byte-for-byte identical. RFC 9110 calls the underlying concept a weak validator: "representation metadata that might not change for every change to the representation data" (RFC 9110 section 8.8.1). An entity tag is strong by default. If an origin's generator "does not satisfy all of the characteristics of a strong validator", the origin server then "MUST mark the entity tag as weak by prefixing its opaque value with 'W/' (case-sensitive)" (section 8.8.3).

Three things it is not. It is not a separate header field. The field is still ETag. W/ is part of the field value, not a parameter. It is not a lesser hash of the same kind as a strong ETag. Strength is a promise the origin makes about its own generator, not something a recipient can test. It is not a ban on byte range requests. A bare Range request carries no validator at all and is answered normally. A weak tag cannot act as a precondition on a range. It also cannot let a client or cache stitch partial responses together. That is because both actions require a strong validator.

The trade in one line: a weak ETag keeps 304 revalidation working for content whose bytes churn while its meaning holds. It gives up every guarantee that depends on exact bytes: If-Match, If-Range, resumable reassembly, and precise variant freshening inside a cache.

How it works

HTTP exposes validator strength in the field value itself. A strong validator "changes value whenever a change occurs to the representation data that would be observable in the content of a 200 (OK) response to GET". A weak validator "might not change for every change to the representation data" (section 8.8.1). This can happen because of clock resolution, because uniqueness cannot be guaranteed, or because of "a desire of the resource owner to group representations by some self-determined set of equivalency". The grammar carries the marker:

  • entity-tag = [ weak ] opaque-tag, with weak = %s"W/" and opaque-tag = DQUOTE *etagc DQUOTE (section 8.8.3). The %s prefix in that ABNF makes W/ case-sensitive. The double quotes are part of the value, not typography.
  • Under weak comparison, "two entity tags are equivalent if their opaque-tags match character-by-character, regardless of either or both being tagged as 'weak'" (section 8.8.3.2). So W/"1" is equivalent to W/"1" and to "1".
  • Under strong comparison, "two entity tags are equivalent if both are not weak and their opaque-tags match character-by-character". A weak tag therefore matches nothing strongly. It does not even match an identical weak tag. The RFC's own table records W/"1" against W/"1" as "no match" for strong comparison and "match" for weak.

Which function applies is fixed by the header field, not chosen by the sender:

  • If-None-Match: a recipient "MUST use the weak comparison function when comparing entity tags for If-None-Match ... since weak entity tags can be used for cache validation even if there have been changes to the representation data" (section 13.1.2). That one sentence is why a weak tag still earns a 304 (Not Modified).
  • 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. Weak validators are useless for lost-update protection on PUT, POST or DELETE.
  • 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). A tag that is received is evaluated with strong comparison.

Here is that flow on the wire:

// Response with weak ETag
HTTP/1.1 200 OK
ETag: W/"post-42-v7"
Content-Type: text/html
Cache-Control: max-age=60

// Conditional request using weak ETag
GET /blog/post-42 HTTP/1.1
Host: example.com
If-None-Match: W/"post-42-v7"

// Server responds 304 if post content unchanged
// Freshness headers repeat; no body is sent
HTTP/1.1 304 Not Modified
ETag: W/"post-42-v7"
Cache-Control: max-age=60

A tag is chosen weak for one of two reasons. The first is deliberate grouping. RFC 9110 offers "the representation of a weather report that changes in content every second, based on dynamic measurements". Such representations "might be grouped into sets of equivalent representations ... with the same weak validator in order to allow cached representations to be valid for a reasonable period of time". A strong tag on such a page would change on every render. It would force a full transfer each time. The origin then "SHOULD change a weak entity tag whenever it considers prior representations to be unacceptable as a substitute for the current representation" (section 8.8.1). That rule, not a timer, is the invalidation signal.

The second reason is that weakness can be forced by sharing. "A validator is weak if it is shared by two or more representations of a given resource at the same time, unless those representations have identical representation data". The RFC's example is an origin sending the same value for a gzip-encoded and an unencoded body. Marking such a tag strong is a specification violation. The alternative is spelled out: "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). Either give each Content-Encoding its own strong tag, or mark the shared one weak.

Much weak tagging is not a decision at all. It is a framework default: "Rails generates weak ETags by default" (Rails Guides, Caching with Rails 7.1). That default is why dynamic HTML nobody configured usually arrives at the edge as W/"...".

Why it matters for a CDN

An edge cache is the party that revalidates most stale objects. That makes validator strength an operational property of the CDN, not a detail of the origin. Weakness changes what the edge can do in four concrete ways.

  • Revalidation still costs no body. When a cache validates, it "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). A weak tag travels in If-None-Match. Weak comparison applies at the origin. A 304 refreshes the stored copy with headers only. The full benefit of conditional requests survives weakness intact.
  • Freshening gets coarser. This is the cost that only bites caches. On receiving a 304, if the response carries strong validators, "each of those strong validators identifies a selected representation for update" and every matching stored response is freshened. But "if the new response contains no strong validators but does contain one or more 'weak validators' ... then the most recent of those matching stored responses is identified for update" (RFC 9111 section 4.3.4). An edge holding several negotiated variants of one URL under one cache key family can freshen only one of them per 304 when the validator is weak.
  • Weak is the honest tag once the edge rewrites bytes. A CDN that applies compression or re-encodes a body has changed the representation data. So the origin's strong tag no longer describes what it is serving. CloudFront states the purpose of downgrading it: converting the strong value to weak "allows viewers, CloudFront, and the origin to treat the compressed and uncompressed versions of an object as semantically equivalent, which reduces unnecessary data transfer" (Amazon CloudFront, ETag header conversion). The alternative, per section 8.8.3.3, is a distinct strong tag per coding plus Vary: Accept-Encoding.
  • Range traffic loses its safety net. A server that generates a 206 (Partial Content) response "MUST generate the following header fields ... if the field would have been sent in a 200 (OK) response to the same request: Date, Cache-Control, ETag, Expires, Content-Location, and Vary" (section 15.3.7). So a weak tag propagates into every partial response. Those parts "can only be safely combined if they all have in common the same strong validator" (section 15.3.7.3). A cache "MAY combine these ranges into a single stored response ... if they all share the same strong validator" (RFC 9111 section 3.4). With a weak tag, an interrupted large download restarts instead of resuming. The edge also cannot assemble a whole object from the ranges it already holds.

What CDNs do

Behaviour differs per provider. It is worth re-testing against current documentation, because most edges weaken or drop a tag as soon as they touch the bytes.

  • Cloudflare: "Cloudflare supports both strong and weak ETags configured at your origin web server". It describes a weak header as indicating "a cached resource is semantically equivalent to the version on the web server but not necessarily byte-for-byte identical". Strong validation is opt-in: "Use Cache Rules to enable strong ETag headers", through the Respect Strong ETags setting. Even with it enabled, Cloudflare converts a strong tag to weak whenever it has to change the encoding to match the visitor. For example, an origin etag: "foobar" comes back as etag: W/"foobar" when the origin sent GZIP and the visitor accepts only Brotli. Weak tags are not automatically safe either: "When using weak ETag headers, 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." An incorrectly formatted, unquoted value is removed rather than weakened (Using ETag Headers with Cloudflare).
  • Amazon CloudFront: when the origin sends a valid strong tag and CloudFront compresses the object, it converts the tag by adding the characters W/ to the front. "when the object from the origin includes a weak ETag header value (a value that begins with the characters W/), CloudFront does not modify this value, and returns it to the viewer as received from the origin". An invalid value, one that does not begin with a double quote or with W/, has its ETag header removed outright (ETag header conversion). On revalidation CloudFront adds "an If-Match or If-None-Match header that contains the ETag value for the expired version of the object". The origin may then "return only an HTTP 304 status code (not modified)". But "If-Modified-Since and If-None-Match conditional requests are not supported when CloudFront is configured to forward cookies (all or a subset)" (Conditional requests).
  • Akamai: the Validate Entity Tag (ETag) behaviour has to be enabled first. Then "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 that option off, "requests with weak ETag values always receive a full 200 OK response from the edge server". The documented match table is stricter than it first looks: a strong If-None-Match of "abc" against a cached W/"abc" is also "No match". So one weak tag stored at the edge disables 304s in both directions (Akamai TechDocs, Validate Entity Tag).
  • Fastly: validator strength is not configurable, but revalidation is validator-driven. "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". Any other status replaces the stored object (Lifetime and revalidation).
  • nginx, as origin or reverse proxy in front of the CDN, applies the rule the RFC asks for: since 1.7.3 "weak entity tags are now preserved on response modifications, and strong ones are changed to weak" (nginx CHANGES). The gzip, ssi and sub body filters call ngx_http_weak_etag(). This function returns early if the value already starts with W/, prefixes W/ to a quoted value, and deletes the header outright if the value does not begin with a double quote (ngx_http_core_module.c). Automatic generation for static resources is on by default via the etag directive. That directive "enables or disables automatic generation of the 'ETag' response header field for static resources".

Watch out for

  • "No ranges" is the wrong summary. "no range preconditions" is the right one. A Range request with no validator still returns 206. RFC 9110 notes that "origin servers and intermediate caches ought to support byte ranges when possible" (section 14.2). What a weak tag forfeits is the If-Range short-circuit (section 13.1.5) and safe reassembly of parts (section 15.3.7.3). Resumable downloads, video seeking and installer delivery need strong tags for that reason, not because ranges are refused.
  • The same pair of tags can pass one check and fail the other. W/"1" and "1" are equivalent under weak comparison, so If-None-Match yields 304. They never match under strong comparison, so If-Match and If-Range fail (section 8.8.3.2). Debugging by "the ETags look the same" misleads. Check which conditional header the client sent.
  • A 304 is never proof of byte equality. Because If-None-Match always uses weak comparison, a 304 means only "your stored copy is still usable". Any code that treats a 304 as confirmation of identical octets is relying on a guarantee HTTP did not give. Examples include checksum caches, delta updaters, and integrity checks.
  • Sharing one value across encodings makes the tag weak whether you mark it or not. If the same value is sent for a gzip-encoded and an unencoded body, "then that validator is weak" (section 8.8.1). Leaving the W/ off does not make it strong. It makes the response non-conformant. It can also corrupt cache updates and range handling downstream.
  • Edges disagree about malformed and weak values. Cloudflare removes an incorrectly formatted ETag rather than weakening it. CloudFront strips a value that begins with neither a double quote nor W/. Akamai has a separate opt-in for matching unquoted values, which it calls "technically malformed" while noting they "appear frequently in real-world origin responses". Test the header your viewers actually receive, not the one your origin emits.
  • Weak tags interact badly with variant caching. A 304 carrying only a weak validator freshens just the most recent matching stored response (RFC 9111 section 4.3.4). Because of this, a URL with several compression or language variants at the edge will keep revalidating the others one at a time. Cloudflare adds a matching operational warning: if the origin varies compression on accept-encoding, "the first compression type will be cached". So consider cache key rules.

Best practice

  • Use a weak tag when the bytes genuinely churn without the meaning changing. Examples: a render timestamp, an ad slot, a rotating session marker. Change its value the moment the older representation would be an unacceptable substitute.
  • Keep tags strong for anything delivered in ranges: video, large images, installers, resumable downloads. A silent downgrade to W/ removes If-Range resumption and range reassembly. Nothing in the response announces the loss.
  • Never share one value across content codings. Emit a distinct strong tag per coding and send Vary: Accept-Encoding. Alternatively, accept the weak marker and its consequences deliberately.
  • Emit the field value quoted, as in "abc123", or weak-marked as W/"abc123", and nothing else. Unquoted values are malformed. Edges variously drop them, weaken them, or match them only behind an option.
  • Do not use weak validators for optimistic concurrency. If-Match is strong-comparison only. So an API behind the edge needs real strong tags for write-conflict detection to work at all.
  • Verify at the edge, not the origin. Request the URL twice: once with Accept-Encoding: gzip, once without. Compare the ETag on each response. A W/ prefix your origin did not send means the CDN re-encoded the body. On Akamai, confirm Support Weak ETags is on. On Cloudflare, check what Respect Strong ETags disables.

Interactive Animation

Loading animation...

Examples

Many web frameworks generate weak ETags by default for dynamic responses. Rails, for example, uses stale? with an object's updated_at timestamp to generate weak ETags automatically.

# Rails controller example
def show
  @article = Article.find(params[:id])
  if stale?(@article)
    render :show
  end
end
# Produces: ETag: W/"a1b2c3d4..."
# Returns 304 if article hasn't been updated

Frequently Asked Questions

A weak ETag is an ETag whose value carries the case-sensitive W/ marker, as in ETag: W/"abc123". It promises that two representations sharing the value are semantically equivalent, not byte-for-byte identical, so it can earn a 304 but cannot serve as a range or If-Match precondition.

Many web frameworks generate weak ETags by default for dynamic responses. Rails, for example, uses stale? with an object's updated_at timestamp to generate weak ETags automatically.

# Rails controller example
def show
  @article = Article.find(params[:id])
  if stale?(@article)
    render :show
  end
end
# Produces: ETag: W/"a1b2c3d4..."
# Returns 304 if article hasn't been updated

Yes. Weak ETag is also known as weak entity tag. A weak ETag is an ETag whose value carries the case-sensitive W/ marker, as in ETag: W/"abc123". It promises that two representations sharing the value are semantically equivalent, not byte-for-byte identical, so it can earn a 304 but cannot serve as a range or If-Match precondition.