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
| Type | Format | Meaning |
|---|---|---|
| Strong | "abc123" | Byte-for-byte identical. Safe for range requests. |
| Weak | W/"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 pair | Request header | Response if unchanged |
|---|---|---|
| ETag / If-None-Match | If-None-Match: "abc123" | 304 Not Modified |
| Last-Modified / If-Modified-Since | If-Modified-Since: Sat, 04 Apr 2026 00:00:00 GMT | 304 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