How HTTP Works

Comparison

ETag vs Last-Modified: HTTP Cache Validators

ETag vs Last-Modified: strong and weak validation, one-second timestamps, conditional request precedence, multi-server consistency, and CDN debugging.

Bottom line: Send both when you have reliable values. ETag identifies a representation version; Last-Modified supplies a date. If-None-Match takes precedence over If-Modified-Since.

By How HTTP WorksReview process
ETag
vs
Last-Modified

TL;DR: Use ETag to identify a representation version and Last-Modified when you have a trustworthy modification date; send both when available. ETag can distinguish changes within one second. If a request sends both If-None-Match and If-Modified-Since, the ETag condition wins.

Side By Side

PropertyETagLast-Modified
Wire valueQuoted opaque tag: "release-17" or W/"release-17"HTTP date: Mon, 05 Oct 2026 09:00:00 GMT
GenerationImplementation-defined; hash or revision are optionsTime the origin believes the representation last changed
ResolutionCan distinguish every representation changeOne second on the wire
StrengthStrong without W/; weak with W/Implicitly weak unless strength can be established
Revalidation requestIf-None-MatchIf-Modified-Since
Main deployment trapDifferent nodes generate different tags for identical contentDeployment timestamps change although content did not

See MDN’s ETag and Last-Modified references. The timestamp resolution and conditional strength rules are in RFC 9110 §8.8.

When To Use Each

ETag: a response with an explicit version. An API that edits an article several times within a second needs more than a second-resolution date. Derive a tag from the rendered representation, or use a revision that changes whenever that representation changes. Do not call a database row revision strong if the rendered response can change independently of that row.

Last-Modified: a response with a meaningful timestamp. A static document or article with a maintained modification time can expose that date. Do not substitute the current request time: that reports a new modification on every response. MDN describes it as a fallback validator, not an exact content fingerprint.

For either strategy, test an unchanged resource, an actual edit, and a redeploy without an edit. The first and third should not generate a new validator merely because a different worker answered.

Strong And Weak Are Different Promises

ETag: "release-17" promises byte identity for matching strong tags. ETag: W/"release-17" permits representations the server considers equivalent even when bytes differ. The string is opaque; there is no required hash algorithm. MDN documents these formats and generation choices.

Weak tags work for If-None-Match revalidation. They do not provide a matching version for If-Match, and clients must not put them in If-Range. A modification date is implicitly weak, but can be strong under the rules in RFC 9110 §8.8.2.2; “Last-Modified is always weak” is too broad. See §13.1 for the conditional comparison rules.

When Both Conditions Arrive

Illustrative exchange for an unchanged representation:

GET /article.json HTTP/1.1
Host: example.com
If-None-Match: "release-17"
If-Modified-Since: Mon, 05 Oct 2026 09:00:00 GMT

HTTP/1.1 304 Not Modified
Date: Mon, 05 Oct 2026 09:05:00 GMT
ETag: "release-17"
Cache-Control: max-age=60

The 304 has no body. Now suppose the tag changes to "release-18" while the date stays the same because two edits occurred within one second. For an otherwise successful GET, the server sends 200 with the new body, not 304: it must ignore If-Modified-Since when If-None-Match is present. This is RFC 9110 §13.2.2, with the explicit ignore rule in §13.1.3.

Multiple Servers And CDNs

Make validator generation part of the deployment contract. Use the same algorithm and inputs on every node. A content digest is one way to get consistency; per-node file metadata needs inspection. Apache’s FileETag documentation lists MTime Size as the default and permits adding INode. Different inode inputs can yield different tags across nodes. Removing inode does not make separately copied modification times agree.

At a CDN, inspect what reaches the client. Cloudflare documents that compression changes can turn a strong ETag into a weak one, even with Respect Strong ETags enabled. With that setting disabled, some combinations of compression and content transformations can instead remove the tag. On a cache miss, Cloudflare needs a full response body to fill its cache; do not assume the origin sees the same conditional request you sent to the edge.

Cloudflare’s Smart Edge Revalidation can use the time an object entered its cache as a synthetic Last-Modified value when the origin supplied neither validator. An edge date therefore need not be your application’s edit time. Compare origin and edge responses before blaming the application.

The Common Mistake

Treating a validator as a freshness policy. ETag does not say how long a cached response can be reused. Set Cache-Control separately; RFC 9111 defines freshness separately from validation.

Another bug is checking the date first and returning 304 before evaluating the tag. Exercise the conflicting-condition case above in your handler: it catches code that works for unchanged files but silently serves an old representation after a same-second edit.

Check On The Wire

Fetch headers with GET, then copy the actual validator values into conditional requests:

curl -sS -D - -o /dev/null https://example.com/article.json
curl -sS -D - -o /dev/null https://example.com/article.json \
  -H 'If-None-Match: "release-17"'
curl -sS -D - -o /dev/null https://example.com/article.json \
  -H 'If-Modified-Since: Mon, 05 Oct 2026 09:00:00 GMT'

The values shown are illustrative. For the precedence check, send a tag you know differs from the current tag along with the current Last-Modified date:

curl -sS -D - -o /dev/null https://example.com/article.json \
  -H 'If-None-Match: "known-old-release"' \
  -H 'If-Modified-Since: Mon, 05 Oct 2026 09:00:00 GMT'

Expect 200 for an otherwise successful GET. If you get 304, verify that the old tag really differs and inspect which layer answered.

Repeat the initial GET against each origin with the same request headers, then through the CDN. Compare ETag, Last-Modified, Content-Encoding, and Vary. Inspect the request and response headers in DevTools as well; a browser result alone cannot tell you what the origin received.

Browse /search