HTML is the highest-value cache target on any estate — expensive to render, first in every waterfall — and the only one where mistakes show one user another user's page. This guide is the safe construction: the split that decides what caches, the rules that enforce it, the fragment patterns that rescue lightly-personalized pages, and the tests that prove it leak-free.
The split: anonymous vs personalized
HTML caching has one organizing question: is this response identical for every visitor who could receive it right now? Anonymous pageviews — no session, no login, no cart — usually answer yes, and on content sites, docs, catalogs and marketing estates they are the traffic majority: all cacheable, all currently being re-rendered per view on most origins. Personalized responses answer no and are not cache candidates as whole pages, ever; the interesting engineering (section three) is converting lightly-personalized pages into a cacheable common page plus personal fragments. Write the split down per route before touching configuration — the exercise is the same four-tier classification the e-commerce guide applies to stores, and the routes where you hesitate are precisely the ones that need a decision, not a default.
The enforcement rules
The split is enforced by rules that fail safe. Cookie bypass, both directions: any request bearing a session/login/cart cookie skips the cache and its response is never stored — the single rule that makes the personalized page structurally unable to reach the shared cache. Set-Cookie discipline: a response carrying Set-Cookie must not be cached (platforms mostly enforce this; verify), which in turn means your origin must stop emitting gratuitous cookies on anonymous pageviews — the analytics or consent cookie set on first view is the classic silent killer of HTML caching, fixable by moving it client-side. Explicit headers: anonymous HTML ships public, s-maxage=long, max-age=short per the two-layer pattern in the Cache-Control guide; personalized responses ship private or no-store so even a misconfigured intermediary refuses them. Fail-safe default: routes that haven't explicitly opted into caching bypass — new-route safety costs a little performance and prevents the incident.
Rescue patterns: common page, personal fragments
Most "personalized" pages are a common page wearing a name badge — identical for everyone except a login indicator, a cart count, a greeting. Three patterns rescue their cacheability. Client-side fragments: cache the common page for all; fetch the personal bits (cart count, user menu) from a small uncached endpoint and render them client-side — the workhorse pattern, one afternoon per template. Edge composition: where you run edge compute, assemble common-cached body plus personal fragment at the edge, keeping personalization out of the browser waterfall — more machinery, better UX, and platform lock-in per the edge platforms field. Vary on a coarse dimension: where content genuinely differs by a small enumerable factor (country, language, logged-in-or-not as two variants), key the cache on a normalized value per the cache-key guide — two or three variants of a cached page beat zero cached pages. What never works: caching per-user pages keyed on session — that's storing your database in the CDN, slowly.
Freshness: purge carries it
Cached HTML stays honest through events, not expiry: publish, update and delete hooks purge the affected pages via the tag scheme from the purge guide (page tags plus entity tags, so editing a product purges every page that renders it), while the edge TTL runs long as a safety net and the browser TTL stays short so clients re-check against the edge cheaply. Add the serve-stale pair so expiry and origin errors never surface, per the serve-stale guide. The freshness SLA worth writing down: content changes visible globally within seconds of the purge firing, with the TTL as the bound on how stale a missed event can leave you — which is also the argument for keeping the purge dispatcher monitored rather than assumed.
Leak tests before and after launch
HTML caching earns production only after adversarial testing. Before launch: the two-browser test (logged-in user in one, anonymous in the other, confirm the anonymous browser never receives any personalized marker — grep responses for usernames, cart counts, CSRF tokens, Set-Cookie); the header audit on every route class; and the deliberately-hostile checks — request personalized routes without cookies, with malformed cookies, via unusual methods, and confirm bypass every time. After launch, make the tests permanent: synthetic checks asserting anonymous responses stay marker-free, an alert on any cache HIT for bypass-classed paths, and per-class hit-ratio monitoring so a silent bypass regression (the analytics cookie returning) shows up as a ratio cliff in the measurement review rather than as a support ticket. The reward for the paranoia is the largest single performance win most estates have left — HTML that renders once and serves everywhere.
