cache-coherence-patterns-reference
Pure-reference catalog of cache-coherence patterns across the request path. Defines the five-tier cache stack (browser → CDN → reverse-proxy → application → data store), the per-tier cache-writing patterns (cache-aside, write-through, write-back, write-around, refresh-ahead), and the canonical invalidation strategies (TTL-only, event-driven purge, surrogate keys, version-tagged URLs, soft purge), plus an anti-pattern table and a worked multi-tenant coherence-test example. Deep detail - the RFC 9111 Cache-Control / Vary / ETag directive tables and the cross-tier coherence + per-tier test surface - lives in references/. Use for pattern selection, Cache-Control header design, and coherence audits; use a cache-key-collision check when the question is whether two requests in an existing system collide on a concrete key scheme. Consumed by redis-cache-tests, cdn-cache-purge-tests, varnish-test-vtc-syntax, browser-cache-control-tests, and the cache-key-collision check.
Install with skills.sh (any agent)
npx skills add testland/qa --skill cache-coherence-patterns-referencecache-coherence-patterns-reference
Overview
Keeping cached values consistent with their source of truth across tiers (browser, CDN, reverse-proxy, application, data store). Wrong coherence shows as stale data; wrong invalidation shows as cache stampedes per cache-stampede-reference. A pure reference consumed by per-tier test skills.
When to use
How to use this reference
The five-tier stack
| Tier | Where | Common TTL | Invalidation |
|---|---|---|---|
| Browser | Cache-Control: private | minutes-hours | TTL only (or Service Worker code) |
| CDN | Cloudflare / Fastly / CloudFront / Akamai | seconds-days | Purge API or surrogate-key tag |
| Reverse proxy | Varnish, nginx | seconds-hours | VCL purge / nginx cache_purge |
| Application | Redis / Memcached / in-process | seconds-minutes | Direct delete / pub-sub broadcast |
| Data store | Postgres query cache, RDS read replicas | seconds | Replication-driven |
A coherence bug at any tier surfaces at the user. The test surface is layered; each tier needs its own coherence tests.
Cache-writing patterns
For application-tier caches (Redis):
| Pattern | Flow | When |
|---|---|---|
| Cache-aside (lazy load) | Read miss → read source → populate → return; Write → invalidate cache | Read-heavy, eventual consistency OK |
| Write-through | Write → write source → write cache (synchronous) | Strong consistency, latency tolerable |
| Write-back | Write → write cache → async write to source | Burst writes; data-loss risk on cache crash |
| Write-around | Write → write source (skip cache); reads do cache-aside | Write-heavy with rare re-reads |
| Refresh-ahead | Background refresh before TTL expires | Predictable read patterns; hot keys |
Invalidation strategies
| Strategy | Mechanism | Trade-off |
|---|---|---|
| TTL-only | Just let it expire | Simple; possibly-stale window = TTL |
| Event-driven purge | Source-of-truth update fires a delete | Coupling; firehose at high write rate |
| Surrogate keys (Fastly, Varnish) | Tag responses; purge by tag | Group-invalidation; coordination cost |
| Version-tagged URLs | /api/users?_v=42; new version = new key | Immutable cache; full deploy per change |
| Soft purge | Mark stale, keep serving until refresh | Used by stale-while-revalidate per stale-while-revalidate-reference |
Worked example: a multi-tenant dashboard endpoint
Scenario: /api/users serves per-tenant dashboard data, is read-heavy, and must never leak one tenant's rows to another. Walk the four decisions from How to use this reference, then the test.
Coherence test (the browser-tier "write → reload → see old" case):
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Cache-Control: public on per-user data | Shared cache leaks data | Use private for user-specific |
Missing Vary: Authorization | Cross-tenant leak | Add to Vary or set private |
s-maxage longer than session lifetime | Logged-out users see another user's data | Match TTL to security window |
| TTL but no purge | Stale-window = TTL even for urgent updates | Implement purge API + use surrogate keys |
ETag generated per-request from now() | Defeats the validation | Stable ETag from content hash |
no-cache instead of no-store for sensitive data | Browser still stores; just revalidates | no-store, no-cache, must-revalidate, private |
| Browser TTL = CDN TTL = origin TTL | Multi-tier amplifies staleness instead of layering it | Origin lowest, CDN longer, browser shortest |
| Cache-aside without write-then-invalidate | Reads see pre-write state for TTL window | Always invalidate on write |
Vary: * | Disables shared cache entirely | Use specific headers |
| Single Cache-Control for HTML + JSON + assets | One-size doesn't fit; HTML often short, assets long | Per-route directives |
Deep references
The contract layer and the audit-and-test surface live in two companion references so this file stays a decision surface:
Limitations
References
Cross-tier coherence problems and the per-tier test surface
View source (opens in new window)Cross-tier coherence problems and the per-tier test surface
Deep reference for cache-coherence-patterns-reference SKILL.md. Consult when auditing an existing multi-tier cache for coherence bugs and when deciding what to test at each tier.
Cross-tier coherence problems
A coherence bug at any tier surfaces at the user; the failure usually lives in the seam between two tiers, not inside one.
| Problem | Where | Detection |
|---|---|---|
| Browser caches stale page after server purge | Browser ignores must-revalidate, or no must-revalidate | E2E test: write → reload → see old |
| CDN serves stale after origin update | Purge didn't propagate or s-maxage too long | E2E: write → purge → read at CDN edge |
| Different Vary at browser vs CDN | CDN strips headers; cache keys diverge | Header-comparison test |
| Layered TTL inversion | s-maxage < max-age → CDN refreshes more often than browser; browser eventually outpaces CDN | Audit the TTL stack |
Vary: Cookie without normalised cookies | Tracker cookies fragment cache; near-zero hit rate | Inspect Vary; normalise |
| Tenant-scoped data with shared Vary | Cross-tenant leak per cross-tenant-data-leak-tests | Add Authorization to Vary or use private |
Testable behaviours by tier
Each tier needs its own coherence tests; the categories below map to the per-tier test skills that consume this reference.
| Tier | Test categories |
|---|---|
| Browser | Cache-Control respected (max-age, no-cache, must-revalidate); ETag round-trip; Vary honoured |
| CDN | Edge hit/miss vs origin; purge API works end-to-end; s-maxage overrides max-age |
| Reverse proxy | VCL purge (varnish-test-vtc-syntax); grace-mode behaviour |
| Application | Cache-aside write-then-invalidate; key collisions |
| Data store | Replication lag (separate concern; out of scope here) |
RFC 9111 HTTP caching directives
View source (opens in new window)RFC 9111 HTTP caching directives
Deep reference for cache-coherence-patterns-reference SKILL.md. Consult when designing the Cache-Control, Vary, and ETag contract that the browser and CDN tiers enforce.
Per www.rfc-editor.org/rfc/rfc9111.html (opens in new window):
Response directives (server → cache)
| Directive | RFC ref | Meaning |
|---|---|---|
max-age=N | §5.2.2.1 | "The response is to be considered stale after its age is greater than the specified number of seconds." |
s-maxage=N | §5.2.2.10 | "For a shared cache, the maximum age specified by this directive overrides... max-age." |
no-cache | §5.2.2.4 | "The response MUST NOT be used to satisfy any other request without forwarding it for validation." |
no-store | §5.2.2.5 | "A cache MUST NOT store any part of either the immediate request or the response." |
must-revalidate | §5.2.2.2 | "Once the response has become stale, a cache MUST NOT reuse that response... until it has been successfully validated." |
private | §5.2.2.7 | "A shared cache MUST NOT store the response (intended for a single user)." |
public | §5.2.2.9 | "A cache MAY store the response even if it would otherwise be prohibited." |
immutable | RFC 8246 | Response body will not change for the lifetime of the URL. Browsers skip revalidation. |
Per RFC 9111 §4.2.4: "A cache MUST NOT generate a stale response unless it is disconnected or doing so is explicitly permitted by the client or origin server." This is the formal basis for stale-while-revalidate per stale-while-revalidate-reference.
Vary - the cache key
Per RFC 9111 §4.1: "When a cache receives a request that can be satisfied by a stored response and that stored response contains a Vary header field, the cache MUST NOT use that stored response without revalidation unless all the presented request header fields nominated by that Vary field value match those fields in the original request."
Practical: Vary: Accept-Encoding, Authorization means "separate cache entries per (Accept-Encoding, Authorization) combination." Missing Vary: Authorization is the canonical cross-tenant cache leak per cross-tenant-data-leak-tests Test 10.
ETag + If-None-Match revalidation
Per RFC 9111 §4.3.1: "Another validator is the entity tag given in an ETag field. One or more entity tags can be used in an If-None-Match header field for response validation."
Pattern: server returns ETag: "abc123"; client sends If-None-Match: "abc123"; server returns 304 Not Modified or 200 OK with new ETag. Bandwidth-efficient but doesn't help latency (still a round-trip).
Sources
Related skills
browser-cache-control-tests
Wraps browser-side Cache-Control testing with Playwright (Cypress for legacy stacks): asserting response Cache-Control headers from Network events, ETag round-trips (If-None-Match → 304), service-worker strategies (Workbox cacheFirst / networkFirst / staleWhileRevalidate), and reload semantics (normal vs hard). Covers MDN Cache-Control + RFC 9111. Use when auditing browser-tier caching in E2E tests. For CDN-edge purge use cdn-cache-purge-tests; for the reverse-proxy tier use varnish-test-vtc-syntax; for an app-tier store use redis-cache-tests; SWR directive semantics live in stale-while-revalidate-reference.
cache-key-discriminator-audit
Audits whether a cache key carries every discriminator the cached response actually depends on, so two requests that must not share a slot cannot collide. Ranks identity discriminators (tenant, user, authorization scope, plan tier) above presentation ones (locale, region, currency, feature flag), maps each missing discriminator to the data-exposure or wrong-content consequence it causes, classifies each key-and-value pair into a critical / high / medium severity band (including the Python lru_cache-on-an-instance-method trap), and writes the fix as a namespaced key builder plus the matching HTTP Vary header. Use when a cache key is being designed or changed, when a per-user or per-tenant response is about to be stored in a shared cache or CDN, or when investigating a report that one user or tenant saw another's data.
cache-stampede-reference
Pure-reference catalog of cache-stampede (thundering-herd) phenomena and mitigations: parallel misses on key expiry trigger simultaneous recomputation (often congestion-collapse), countered by three families - locking, external recomputation near-expiry, and probabilistic early expiration via XFetch (`(time() - delta * beta * log(rand(0,1))) >= expiry`). Use when designing cache-refresh strategy or diagnosing a stampede incident. This is the single failure-mode pattern; for the broader multi-tier pattern catalog use cache-coherence-patterns-reference, for the stale-while-revalidate / stale-if-error extensions use stale-while-revalidate-reference; a reference consumed by redis-cache-tests, not a runnable test.
cdn-cache-purge-tests
Wraps CDN cache-purge testing patterns for Cloudflare (POST /zones/{zone_id}/purge_cache, single-file / everything / cache-tags / hostname / prefix), Fastly (POST purge-by-key / purge-all, surrogate-keys via Surrogate-Key header), and CloudFront (CreateInvalidation API + paths). Covers end-to-end test patterns (write origin → trigger purge → assert edge serves fresh), purge-propagation delay testing (typically 1-30s globally), surrogate-key + cache-tag patterns for group-purge, and Cache-Status header verification (cf-cache-status: HIT/MISS/BYPASS). Use when designing or auditing CDN cache-invalidation workflows.
memcached-tests
Wraps Memcached cache testing against a real container: an inline set/get/expire/no-persistence worked example, with the exhaustive protocol-command tests (set/get/add/cas/incr/decr, TTL 0=never-expire / 30-day Unix-timestamp boundary) and the LRU-eviction, consistent-hashing distribution, ElastiCache Auto Discovery, and CI-wiring deep-dives in references/. Use when writing tests for an application that uses Memcached as its primary cache, when verifying ElastiCache Memcached cluster behaviour, or when contrasting Memcached eviction and distribution semantics against Redis.
redis-cache-tests
Wraps Redis cache testing: EXPIRE / PEXPIRE / TTL verification (Redis 7+ NX/XX/GT/LT flags), cache-aside write-then-invalidate (write source → DEL key → assert fresh read), eviction under memory pressure (maxmemory + allkeys-lru), pub/sub invalidation across nodes, and tenant key-namespacing. Use when Redis is the app's primary cache. For a Memcached app-tier store use memcached-tests; for the browser/HTTP tier use browser-cache-control-tests; a runnable test, not the cache-stampede-reference or cache-coherence-patterns-reference catalogs.
stale-while-revalidate-reference
Pure-reference catalog of RFC 5861's stale-while-revalidate + stale-if-error Cache-Control extensions. Defines stale-while-revalidate=N (caches MAY serve a stale response while asynchronously revalidating, up to N seconds after expiry) and stale-if-error=N (caches MAY serve stale on 5xx upstream errors). Distinguishes from RFC 9111's must-revalidate (forbids serving stale) and from manual cache-aside refresh (synchronous). Covers the interaction with the freshness lifetime (max-age) and the cache-stampede-mitigation properties. Use when designing the cache-refresh boundary or auditing existing Cache-Control headers.
varnish-test-vtc-syntax
Wraps the varnishtest CLI + VTC (Varnish Test Case) syntax for testing VCL configurations. Covers the VTC test-file format (varnishtest scripts with server { ... } + client { ... } + varnish v1 -vcl+backend { ... } blocks), the grace-mode + saint-mode behaviours (stale-while-revalidate + stale-if-error equivalents in VCL), the PURGE method handler pattern (vcl_purge subroutine + ACL guards), and surrogate-key invalidation via xkey vmod. Use when authoring or testing Varnish-based caching layers.