Cache-Control for HTML Documents: A Practical, Deploy-Safe Policy

This guide assembles the pieces of HTTP Cache-Control Headers Explained into a policy for the hardest resource to cache, within Advanced Caching Strategies & CDN Architecture. Hashed assets are easy: their URLs change when content changes, so they can be cached for a year. HTML is different. Its URL never changes, it references the current release's assets, and it must update promptly when content or code changes. Cache it too long and visitors see stale pages that reference deleted assets; do not cache it at all and every visit pays origin TTFB, which sits directly in the first phase of LCP.

The policy that works for most sites splits responsibilities: browsers keep HTML only briefly (or revalidate each time), the CDN keeps it for longer and is purged on change, validators make revalidations cheap, and stale-while-revalidate hides refresh latency. Each piece addresses a specific failure mode, and the combination delivers sub-200ms TTFB for most visits without risking stale releases.

Layers of an HTML caching policy The layers of a robust HTML caching policy from browser TTL to purging. Layers of an HTML caching policy Browser: max-age=0 + ETag always revalidates, cheaply — browsers cannot be purged CDN: s-maxage=600 serves from the edge for minutes — the TTFB win CDN: stale-while-revalidate refreshes in the background so nobody waits for a refresh Purge on deploy and publish tag or URL purge so changes appear immediately

Rapid Diagnosis

  • Read current HTML headers for each template (curl -sI). Typical problems: no-store everywhere, max-age=3600 for browsers, or nothing at all.
  • Check CDN status for HTML (HIT/MISS/DYNAMIC). If HTML is never cached at the edge, TTFB equals origin latency.
  • Deploy and test staleness. After a deploy, how long until a browser that visited before sees the new page? If the answer is "up to an hour", browser TTLs are too long.
  • Check for broken pages after deploys. Stale HTML referencing deleted asset hashes is the classic symptom — see fixing stale HTML after a deploy.

Root Cause Analysis

1. Browser TTLs on HTML. A max-age of minutes or hours on HTML means browsers keep old versions that you cannot purge.

2. Uncached HTML at the CDN. Default CDN behaviour skips HTML; without explicit rules every request goes to the origin.

3. Personalisation in shared HTML. Pages that read cookies cannot be cached publicly until personal parts are split out.

4. No purge path. Without purging, teams keep CDN TTLs short, which keeps hit ratios low.

TTFB p75 for an article template by HTML policy Bar chart of p75 time to first byte for the same article template under four HTML caching policies. TTFB p75 for an article template by HTML policy no-store (origin every time) 620ms no-cache, no CDN caching 540ms max-age=0, s-maxage=600 110ms + stale-while-revalidate 85ms 200ms

Step-by-Step Resolution

1. Set the split policy on public HTML

nginx
location / {
  add_header Cache-Control "public, max-age=0, must-revalidate, s-maxage=600, stale-while-revalidate=86400, stale-if-error=86400" always;
  # trade-off: s-maxage=600 means a missed purge can leave stale HTML at the edge
  # for up to ten minutes. That is acceptable only because purges run on every
  # deploy and publish; without them, use a much shorter s-maxage.
}

Expected outcome: browsers revalidate against the nearby CDN; the CDN serves cached HTML and refreshes in the background.

2. Emit validators

Make sure HTML responses carry a stable ETag so browser revalidations against the CDN return 304s — see ETag vs Last-Modified validators.

3. Purge on deploy and on content changes

On deploy, purge HTML (by tag such as html or by template tags); on content publish, purge the affected pages' tags. Never purge hashed assets.

bash
# Cloudflare example: purge all HTML tagged at the origin with Cache-Tag: html.
curl -sX POST "https://api.cloudflare.com/client/v4/zones/$ZONE/purge_cache" \
  -H "Authorization: Bearer $CF_TOKEN" -H "Content-Type: application/json" \
  --data '{"tags":["html"]}'
# trade-off: purging all HTML on every deploy causes a brief burst of origin
# traffic; tiered caching and request collapsing keep it manageable.

4. Use private policies for personalised HTML

For logged-in or personalised pages that cannot be split yet, use private, no-cache — browser revalidation only, no CDN caching — and plan to move them to a shared-shell architecture.

HTML policies by page type Recommended Cache-Control values for different kinds of HTML pages. HTML policies by page type Page type Browser CDN Public content and marketing max-age=0 + ETag s-maxage=600 + SWR + purge Listings and search max-age=0 + ETag s-maxage=60 + SWR Personalised (not yet split) private, no-cache not cached Sensitive account pages no-store not cached

Verification

After deploying the policy, request a page twice through the CDN (hit on the second request, Age increasing), reload in a browser (304 for HTML), and run a deploy: within seconds of the purge, a fresh request should return the new HTML. In RUM, TTFB p75 for public templates should approach edge latency, and LCP should improve correspondingly.

Worked Example: A Marketing Site on a Headless CMS

A marketing site served HTML from a Node SSR server with Cache-Control: no-cache and no CDN rules; TTFB p75 was 480ms globally and over 900ms in Asia. The team added the split policy with s-maxage=600, tagged pages by CMS entry, purged tags from a CMS webhook on publish and purged html on deploy. TTFB p75 fell to 70ms worldwide, LCP p75 improved by 400ms, and editors saw published changes live within seconds thanks to the webhook purge.

Common Mistakes

  • Long browser max-age on HTML. You cannot purge browsers; stale versions persist for the full TTL.
  • Short CDN TTLs instead of purging. They keep hit ratios low and TTFB high.
  • Purging hashed assets on deploy. Unnecessary and harmful; only HTML (and data) need purging.
  • Missing stale-if-error. Without it, an origin outage turns cached HTML into errors once TTLs expire.

Edge Cases

Prerendered and static sites. Static HTML can use the same policy; purge on deploy replaces rebuilding caches.

Streaming SSR. Cached HTML is served whole from the edge; streaming matters only for misses.

Early Hints. CDNs can send 103 Early Hints for cached HTML, letting browsers start fetching critical assets even before the HTML arrives — see using 103 Early Hints behind a CDN.

Service workers. If a service worker caches HTML, it becomes another layer that must be updated on deploy.

FAQ

Why max-age=0 instead of no-cache for browsers?

With must-revalidate they behave the same for browsers; max-age=0 pairs naturally with s-maxage in one header. Either is fine as long as validators are present.

How long should s-maxage be?

As long as your purge process is reliable — minutes to hours for content sites. Without purging, keep it short (30–60 seconds) to bound staleness.

Does stale-while-revalidate risk serving old content after a deploy?

Only if the purge fails; purges remove entries regardless of SWR windows. SWR applies to entries that expire naturally, letting one request trigger a background refresh while others are served immediately.

Should HTML vary on Accept-Encoding?

Yes — CDNs and servers handle compressed variants with Vary: Accept-Encoding, which is safe because the value set is small. Avoid other Vary values on HTML.

What about HTML on a static host without purge APIs?

Use short s-maxage (or the host's built-in invalidation on deploy) and max-age=0 for browsers. Many static hosts invalidate their CDN automatically on each deploy.

How do I handle HTML for A/B tests?

Assign the arm at the edge, include it in the cache key, and keep the same split policy for each variant. Avoid client-side experiment rewrites of cached HTML, which add flicker and layout shift.

Should error pages be cached?

Cache 404 pages briefly (seconds to a minute) to absorb bot traffic, and never cache 5xx responses for long. Use stale-if-error so an origin failure serves the last good page instead of an error.

Does this policy work with HTTP/3 and Early Hints?

Yes. Caching headers are independent of the transport, and cached HTML is the best case for 103 Early Hints because the CDN can send hints instantly from its cached knowledge of the page's critical assets.

/html>