Appendix B · HTTP Caching Headers Reference
A complete reference for every HTTP header relevant to CDN caching. Cross-referenced against RFC 7234 (HTTP/1.1 Caching), RFC 9110 (HTTP Semantics), RFC 9111 (HTTP Caching), and RFC 5861 (stale extensions).
Cache-Control
The primary mechanism for controlling caching behavior. Both request and response headers; semantics differ.
Response Cache-Control Directives
max-age=<seconds>
Content is fresh for this many seconds after the Date response header.
Cache-Control: max-age=3600
After 3600 seconds, the cached response is stale. CDN must revalidate or refetch from origin.
Gotcha:
max-ageis relative toDate, not to when the CDN received the response. If origin is slow and the response travels for 5 seconds, the CDN’s effective max-age is reduced by 5 seconds.
s-maxage=<seconds>
Overrides max-age for shared caches only (CDN, proxy). Does not
affect browser cache. Ideal for: short browser TTL + long CDN TTL.
Cache-Control: max-age=60, s-maxage=3600
Browser caches the page for 1 minute; CDN caches it for 1 hour.
no-cache
Does not mean “don’t cache”. Means: store the response, but revalidate
with origin before every use. Equivalent to max-age=0, must-revalidate.
Cache-Control: no-cache
The CDN will send a conditional request (If-None-Match or If-Modified-Since)
for every cache hit. Origin returns 304 if unchanged (fast), or 200 + new body.
no-store
Do not cache at all. Not in memory, not on disk, not in shared caches.
Cache-Control: no-store
Use for: authentication tokens, session data, personalized responses.
private
Cache only in the browser (private cache). CDN/proxies must not cache.
Cache-Control: private, max-age=3600
Use for: user-specific pages (shopping cart, account dashboard).
public
Cache in all caches (browser + CDN), even for responses to requests
with Authorization headers.
Cache-Control: public, max-age=86400
Without public, responses to authenticated requests are not cached
by CDNs by default (even if max-age is set).
must-revalidate
Once stale, must revalidate before serving. Do not serve stale content even if the origin is unavailable.
Cache-Control: max-age=3600, must-revalidate
Contrast with stale-if-error which allows serving stale when origin fails.
proxy-revalidate
Same as must-revalidate but applies only to shared caches (CDN/proxy),
not browsers.
immutable
The response body will not change during its freshness lifetime.
Browser (and some CDNs) will not send conditional revalidation requests
until after max-age expires.
Cache-Control: public, max-age=31536000, immutable
Use for: hashed assets (main.abc123.js), HLS segments, versioned files.
Saves one conditional request per asset per page load.
stale-while-revalidate=<seconds>
After max-age expires, continue serving the stale response while
revalidating in the background. See Lab 7.
Cache-Control: max-age=60, stale-while-revalidate=600
From 60s to 660s after Date: serve stale, trigger background revalidation.
After 660s: must revalidate before serving.
stale-if-error=<seconds>
If origin returns 5xx (or is unreachable), serve the stale cached response for up to this many seconds beyond the normal expiry.
Cache-Control: max-age=3600, stale-if-error=86400
Serve stale for up to 24 hours during origin outage. Combine with
stale-while-revalidate for both performance and resilience.
no-transform
Prohibit intermediate caches (CDN) from transforming the response body. Prevents CDN from applying compression, image optimization, or minification.
Request Cache-Control Directives
no-cache
Force the CDN to revalidate with origin before returning a cached response. Used by browsers when the user clicks “Refresh”.
no-store
Request that the response not be cached (hint; servers may ignore).
max-age=<seconds> (request)
Accept only responses not older than this many seconds (fresh or revalidated).
max-stale=<seconds> (request)
Accept stale responses up to this many seconds past their expiry.
min-fresh=<seconds> (request)
Accept only responses that will remain fresh for at least this many more seconds.
only-if-cached (request)
Return a cached response or 504 Gateway Timeout. Used by clients that want to avoid network requests.
Expires (Legacy)
The original HTTP/1.0 cache expiry mechanism. Specifies an absolute date:
Expires: Wed, 21 Oct 2025 07:28:00 GMT
Cache-Control: max-age overrides Expires when both are present.
Expires is still useful for backwards compatibility with very old clients
and proxies that don’t understand Cache-Control.
Gotcha: If
Expiresis in the past, the response is immediately stale. If the value is malformed or invalid, it’s treated as expired.
ETag
An opaque validator for a specific version of a resource:
ETag: "686897696a7c876b7e"
ETag: W/"686897696a7c876b7e" (weak ETag)
Strong ETag ("...") means byte-for-byte identical content.
Weak ETag (W/"...") means semantically equivalent content (may have
different whitespace, compression, etc.).
Revalidation with ETag:
Client: GET /resource HTTP/1.1
If-None-Match: "686897696a7c876b7e"
Origin: 304 Not Modified (if ETag matches — no body)
200 OK + new body + new ETag (if changed)
ETag best practices:
- Use content hash (SHA-1, MD5, xxhash) for strong ETags
- For database-backed content: use
version_numberorupdated_at - Avoid using timestamps alone — they don’t reflect content changes and can cause spurious mismatches
Last-Modified / If-Modified-Since
The older revalidation mechanism (HTTP/1.0):
Response: Last-Modified: Tue, 15 Nov 2024 08:12:31 GMT
Request: If-Modified-Since: Tue, 15 Nov 2024 08:12:31 GMT
Response: 304 Not Modified (if not modified since that date)
200 OK + new body (if modified)
Use ETag when possible — it’s more precise. Last-Modified is only
second-resolution; files updated multiple times per second may have
the same Last-Modified but different content.
Vary
Tells caches that the response varies based on request headers:
Vary: Accept-Encoding
Vary: Accept-Language
Vary: Accept, Accept-Encoding
With Vary: Accept-Encoding, the CDN stores a separate cached response
for each Accept-Encoding value:
GET /page.html Accept-Encoding: gzip → cache key: /page.html#gzip
GET /page.html Accept-Encoding: br → cache key: /page.html#brotli
GET /page.html (no Accept-Encoding) → cache key: /page.html#identity
Warning: Vary: User-Agent or Vary: Cookie creates cardinality
explosion — a separate cache entry per unique User-Agent string (thousands).
Avoid unless necessary. CDNs often ignore Vary: Cookie for this reason.
Surrogate-Control / Surrogate-Key (CDN-specific)
Not in RFCs; vendor-specific cache control for CDN-only directives:
Surrogate-Control: max-age=86400
This header is intended for the CDN only. The CDN strips it before
forwarding to the browser, so the browser uses its own Cache-Control.
Surrogate-Key (Fastly) / Cache-Tag (Cloudflare):
Surrogate-Key: article-123 author-456 category-sports
Cache-Tag: article-123 author-456 category-sports
Associates the cached response with logical tags. Enables instant purge by tag rather than by URL. See Lab 9.
CDN-Cache-Control
A CDN-specific Cache-Control variant proposed as a standard (draft):
CDN-Cache-Control: max-age=600
Cache-Control: max-age=60
CDN respects CDN-Cache-Control (600s TTL) while browsers respect
Cache-Control (60s TTL). Supported by Cloudflare, Fastly, and others.
More explicit than s-maxage because it targets CDNs specifically rather
than all shared caches.
Pragma: no-cache (Legacy)
HTTP/1.0 equivalent of Cache-Control: no-cache. Ignore in new code;
handle for backwards compatibility:
Pragma: no-cache → treated as Cache-Control: no-cache by modern caches
Age
Set by the CDN/proxy to indicate how old a cached response is:
Age: 1234
Age: 1234 means this response was fetched from origin 1234 seconds ago.
Remaining freshness = max-age - Age. If Age >= max-age, the response
is stale even before it leaves the CDN.
Warning (Deprecated in RFC 9111)
Formerly used to indicate stale or revalidation state:
Warning: 110 - "Response is Stale"
Warning: 214 - "Transformation Applied"
RFC 9111 deprecated all Warning headers. Do not generate them; ignore if received.
Quick Reference Table
| Header | Direction | Purpose |
|---|---|---|
Cache-Control: max-age | Response | Cache TTL in seconds |
Cache-Control: s-maxage | Response | CDN-only TTL override |
Cache-Control: no-cache | Response | Revalidate before use |
Cache-Control: no-store | Response | Never cache |
Cache-Control: private | Response | Browser cache only |
Cache-Control: public | Response | All caches including CDN |
Cache-Control: immutable | Response | Never revalidate during freshness |
Cache-Control: stale-while-revalidate | Response | Async background refresh |
Cache-Control: stale-if-error | Response | Serve stale on origin failure |
ETag | Response | Version identifier |
Last-Modified | Response | Last change timestamp |
Vary | Response | Differentiate cache by request headers |
Age | Response | Time in cache (seconds) |
Expires | Response | Absolute expiry date (legacy) |
Surrogate-Control | Response | CDN-only TTL (stripped before browser) |
Surrogate-Key / Cache-Tag | Response | Logical purge grouping |
If-None-Match | Request | Conditional request by ETag |
If-Modified-Since | Request | Conditional request by date |
Cache-Control: no-cache | Request | Force CDN revalidation |