Compare
CF CDN vs Cloudflare: What the Abbreviation Actually Means
CF CDN vs Cloudflare in 2026: Abbreviation Decoder Ask ten engineers what "CF CDN" means and you will get two answers, ...
You can have correct ETag headers and working 304 revalidation across origin and CDN edge in about 30 minutes on an existing nginx origin. The payoff is measurable: a 304 Not Modified response is roughly 150–400 bytes of headers against a full response body, so revalidating a 250 KB bundle instead of re-downloading it removes about 99.8% of the bytes. This playbook covers emitting deterministic ETag headers, choosing strong versus weak validators, wiring conditional requests through the edge, and verifying that an If-None-Match request actually returns 304 rather than a silent 200.

An ETag is an opaque validator the origin attaches to a representation. The cache stores it, and when the entry goes stale it re-asks the origin with If-None-Match carrying that value. If the current representation still matches, the origin answers 304 with no body and the cache resets its freshness timer.
An ETag validation exchange, standardized in RFC 9110, replaces a full response body with a 304 Not Modified that carries only headers, typically 150 to 400 bytes on the wire. For a 250 KB JavaScript bundle revalidated hourly, that removes roughly 99.8% of the bytes per check, which is why cache validation, not cache lifetime, is what keeps revalidation cheap in 2026.
Strong validators promise a byte-for-byte identical representation. Weak validators, prefixed with W slash, promise only semantic equivalence. If-None-Match uses weak comparison, so both kinds revalidate fine. If-Range and If-Match use strong comparison, so a weak ETag silently disables byte-range resumption and optimistic-concurrency writes that rely on 412 Precondition Failed. Media and large downloads need strong validators; HTML and JSON usually do not care.
Request the same three URLs from every origin node: one HTML document, one API JSON response, one static asset. Record ETag, Last-Modified, Cache-Control, and Vary for each. If the ETag differs between nodes for identical bytes, cache validation is already broken and every revalidation will cost you a full body.
nginx derives its ETag from file modification time and content length, so a deploy that rewrites mtimes changes every validator even when the bytes are unchanged. Apache 2.2 defaulted to including the inode, which guarantees divergence across nodes. Pin Apache explicitly:
FileETag MTime Size
The durable fix is a content hash computed at build time and served as the ETag by your application layer. Cost of that fix: one build step and a hash lookup per request. Benefit: validators survive redeploys, rsyncs, and node replacement.
Fingerprinted build output does not need validation at all; a one-year immutable TTL is strictly cheaper than any conditional request. Everything without a content hash in its URL gets a short shared TTL and leans on the ETag header.
server {
listen 443 ssl;
http2 on;
server_name YOUR_HOSTNAME; # substitute your public hostname
root SITE_ROOT_PATH; # substitute the absolute path to your document root
# Fingerprinted assets: no validation needed, ever
location /static/ {
etag off;
add_header Cache-Control "public, max-age=31536000, immutable";
}
# HTML and JSON: short shared TTL, the ETag does the real work
location / {
etag on;
gzip_static on;
add_header Cache-Control "public, max-age=0, s-maxage=60, stale-while-revalidate=30";
add_header Vary "Accept-Encoding";
}
}
Why max-age=0 with s-maxage=60: browsers revalidate on every navigation while the edge absorbs the load for a minute. Note that add_header does not inherit into nested locations, so repeat it where you nest.
Compressed and uncompressed representations are different bytes and must not share a strong ETag. nginx downgrades the validator to weak when its gzip filter rewrites the body, which is correct behavior. Always send Vary Accept-Encoding alongside it. If you serve Brotli and gzip variants from a build pipeline, give each variant its own validator.
Configure the CDN to forward If-None-Match to the origin unchanged and to accept 304 as a cache refresh rather than an error. Enable request coalescing or origin shielding so a thundering herd of expiring edge entries produces one conditional request upstream, not ten thousand.
Send two requests to the same URL, the second carrying the ETag returned by the first. Set the request headers explicitly with your HTTP client of choice.
Request 1 (cold)
GET /pricing.html HTTP/1.1
Host: YOUR_HOSTNAME
HTTP/1.1 200 OK
ETag: "6a1f2d-2c40"
Cache-Control: public, max-age=0, s-maxage=60, stale-while-revalidate=30
Vary: Accept-Encoding
Content-Length: 11328
Request 2 (revalidation)
GET /pricing.html HTTP/1.1
Host: YOUR_HOSTNAME
If-None-Match: "6a1f2d-2c40"
HTTP/1.1 304 Not Modified
ETag: "6a1f2d-2c40"
Cache-Control: public, max-age=0, s-maxage=60, stale-while-revalidate=30
Vary: Accept-Encoding
Expected values: status 304, no body, and the same ETag echoed back. A 304 that omits Cache-Control will refresh nothing, because the cache has no new freshness lifetime to apply. In origin logs, healthy revalidation shows as 304 responses with bytes-sent under 500. Repeat the second request against every origin node.
Set etag off in the affected location, restore the previous Cache-Control line, reload the server, then purge the affected paths at the edge. Purging matters: caches that stored the new validator will keep sending If-None-Match against an origin that no longer emits one, and you will get 200s with full bodies until the entries expire. Watch origin egress for 15 minutes after the purge to confirm it settles.
| Symptom | Cause | Fix |
|---|---|---|
| Edge always pulls a full 200, never a 304 | A middleware layer strips If-None-Match, or the framework regenerates the body before comparing | Whitelist If-None-Match in proxy request headers; move validator comparison ahead of body rendering |
| Same file, different ETag per node | Inode in the validator, or mtimes rewritten by deploy | Pin to MTime and Size, preserve mtimes on copy, or emit a build-time content hash |
| Range requests and video seeking fail or restart | Weak validator used with If-Range, which requires strong comparison | Disable on-the-fly compression for media paths so the strong ETag survives |
| Clients revalidate on every single request | The 304 omits Cache-Control, so freshness is never extended | Include Cache-Control, ETag, Date, and Vary on all 304 responses |
| Wrong compressed variant served to some clients | Encoding variants share one ETag and Vary is missing | Add Vary Accept-Encoding and give each encoding its own validator |
Most broken cache validation traces back to one of two things: a validator that is not stable across origin nodes, or a 304 that forgets to carry Cache-Control.
Track the ratio of 304 to 200 responses at the origin for revalidatable paths. On a content site with a 60-second shared TTL, that ratio should sit well above 10:1 within a day. Run the arithmetic on your own volume: 5 million daily revalidations of a 250 KB asset move about 1,220 GB as full responses versus roughly 1.5 GB as 304s, close to 830x fewer bytes for the same freshness guarantee.
Two honest trade-offs. Application-generated ETags cost CPU on every miss, and for cheap-to-render responses a Last-Modified header with one-second granularity is simpler and nearly as effective. And stale-while-revalidate hides revalidation latency at the cost of serving briefly outdated bytes, which is unacceptable for pricing pages and inventory counts.
Where the revalidation logic runs matters as much as the origin config, so check that your provider lets you set conditional-request and TTL behavior per path rather than per property. BlazingCDN, for instance, exposes cache rules at that granularity; comparable per-path CDN cache configuration features are worth confirming before you commit to an edge design that depends on them.
A strong ETag guarantees the representation is byte-for-byte identical; a weak ETag, written with a W slash prefix, guarantees only semantic equivalence. Both work for If-None-Match revalidation because that uses weak comparison. Only strong validators satisfy If-Range and If-Match, so weak ETags break byte-range resumption and optimistic-concurrency updates.
Use an ETag header when content can change more than once per second or when generation is not filesystem-backed, since Last-Modified has one-second granularity. Send both when you can: caches prefer If-None-Match and ignore If-Modified-Since when both are present, per RFC 9110, but older clients and intermediaries still fall back to dates.
The usual cause is that the request header never reaches the comparison logic, either because a proxy strips it or because the application renders the body before evaluating the validator. Second most common: the validator itself changed, typically because a deploy rewrote file modification times or a different origin node answered.
No. Content-hashed filenames with a one-year max-age and the immutable directive eliminate revalidation entirely, which is cheaper than any conditional request. Reserve ETag headers for URLs whose content changes while the path stays constant: HTML documents, API responses, feeds, and manifests.
Add status code and bytes-sent to an origin log query filtered to revalidatable paths, then chart 304 count against 200 count for seven days. If 304s are under 10% of that traffic, your validators are not stable across nodes and the fix is Step 2, not a longer TTL. Then repeat the two-request check against each origin node individually. Divergent ETags for identical bytes is the single most common reason cache validation quietly costs full-body transfers instead of saving them.
Compare
CF CDN vs Cloudflare in 2026: Abbreviation Decoder Ask ten engineers what "CF CDN" means and you will get two answers, ...
Video - Bandwidth & Costs