HTTP ETag Explained — Cache Validation Without Full Downloads

Updated April 2026

An ETag is a fingerprint for a response. When cached content might be stale, the browser sends the ETag back to the server. If the content hasn't changed, the server returns 304 Not Modified with no body — saving the full download.

How it works

# First request — server sends content + ETag
GET /style.css HTTP/1.1

HTTP/1.1 200 OK
ETag: "abc123def456"
Cache-Control: no-cache
Content-Length: 12400
[full CSS body]

# Second request — browser sends ETag back
GET /style.css HTTP/1.1
If-None-Match: "abc123def456"

# Response if unchanged — no body sent
HTTP/1.1 304 Not Modified
ETag: "abc123def456"
Cache-Control: no-cache

The 304 response has no body — just headers. For large files this saves significant bandwidth and load time.

Strong vs weak ETags

TypeFormatMeaning
Strong"abc123"Byte-for-byte identical. Safe for range requests.
WeakW/"abc123"Semantically equivalent but may differ in minor ways (e.g. whitespace). Nginx generates weak ETags by default.

ETag vs Last-Modified

Both do cache validation. ETags are more precise — Last-Modified only has 1-second granularity, so files changed within the same second look identical. ETags are preferred.

Header pairRequest headerResponse if unchanged
ETag / If-None-MatchIf-None-Match: "abc123"304 Not Modified
Last-Modified / If-Modified-SinceIf-Modified-Since: Sat, 04 Apr 2026 00:00:00 GMT304 Not Modified

Nginx — ETag configuration

Nginx enables ETags by default for static files. For proxied content, configure manually:

# ETags are on by default for static files
# Verify with:
curl -sI https://yoursite.com/style.css | grep -i etag

# Disable ETags (not recommended)
etag off;

# For proxy — pass upstream ETag through
proxy_pass http://backend;
proxy_pass_header ETag;

Cloudflare and ETags

Cloudflare converts strong ETags to weak ETags when compression is applied (because the compressed body differs from the original). This is correct behaviour — the ETag still works for validation, it's just marked as weak.

# Origin sends:
ETag: "abc123"

# Cloudflare may transform to:
ETag: W/"abc123"  # After applying Brotli compression

ETags with CDNs and multiple servers

If you have multiple origin servers, they may generate different ETags for the same file — breaking validation. Fix by generating ETags from file content (hash) rather than inode/mtime:

# Nginx — use file size and last modified time (default)
# These can differ across servers

# Better: generate ETag from content hash in your app
# Express example:
const crypto = require('crypto')
const hash = crypto.createHash('md5').update(fileContent).digest('hex')
res.setHeader('ETag', `"${hash}"`)
res.setHeader('Cache-Control', 'no-cache')

The best Cache-Control + ETag combination

# HTML pages — always revalidate, use ETag for efficiency
Cache-Control: no-cache
ETag: "abc123"

# Static assets — long cache, no revalidation needed
Cache-Control: public, max-age=31536000, immutable
# (No ETag needed — immutable means never revalidate)

# API responses — revalidate, save bandwidth on unchanged data
Cache-Control: no-cache
ETag: "data-hash-abc123"
Audit your cache headers → EdgeFix Visualise Cache-Control → Cache Simulator