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.
Install with skills.sh (any agent)
npx skills add testland/qa --skill stale-while-revalidate-referencestale-while-revalidate-reference
Overview
stale-while-revalidate (SWR) and stale-if-error (SIE) are Cache-Control extensions defined in RFC 5861 (opens in new window). RFC 9111 references them as informative; they are widely implemented by browsers, CDNs (Cloudflare, Fastly, CloudFront), and reverse proxies (Varnish via grace mode).
Both directives extend the cache lifetime beyond freshness, but in different ways:
| Directive | When stale-serve happens | What revalidation looks like |
|---|---|---|
stale-while-revalidate=N | Up to N seconds after max-age expires | Background async; client sees stale |
stale-if-error=N | When origin returns 5xx, up to N seconds after max-age | Synchronous on error; client sees stale instead of 5xx |
When to use
stale-while-revalidate
Per RFC 5861 §3 (opens in new window):
"When present in a response, caches MAY serve the response in which it appears after it becomes stale, up to the indicated number of seconds."
Syntax: Cache-Control: max-age=60, stale-while-revalidate=300.
Lifecycle
The client always sees a response in steps 1-3. Stampedes are eliminated in step 2 - only the first request triggers revalidation; others coast on the stale value.
When the async revalidation fails
The cache may keep serving stale until the SWR window expires. Vendors differ here:
Test the actual behaviour per vendor.
stale-if-error
Per RFC 5861 §4 (opens in new window):
"A cached stale response MAY be used to satisfy the request, regardless of other freshness information, provided staleness hasn't exceeded the specified limit."
Applies to status codes 500, 502, 503, 504.
Syntax: Cache-Control: max-age=60, stale-if-error=86400.
Pattern: 1-minute freshness, but 1-day grace if origin is down.
Composition with stale-while-revalidate
Cache-Control: max-age=60, stale-while-revalidate=300, stale-if-error=86400Interpretation:
| Time | What happens |
|---|---|
| 0-60s | Cache hit, fresh |
| 60-360s | Cache hit, stale; async revalidate |
| 60-86400s and origin returns 5xx | Cache serves stale (last good value) |
| 360s+ if revalidation succeeds | Normally back to fresh after revalidation |
| 86400s + origin down | Truly stale + error |
Interaction with must-revalidate
Per RFC 9111: must-revalidate "Once the response has become stale, a cache MUST NOT reuse that response to satisfy another request until it has been successfully validated."
must-revalidate and stale-while-revalidate are mutually exclusive in spirit. Setting both is undefined behaviour by RFC 9111 + RFC 5861 reading; most caches honour the strictest (must-revalidate wins).
For SWR / SIE to work, don't add must-revalidate.
Per-vendor support
| Cache | SWR support | SIE support | Caveat |
|---|---|---|---|
| Cloudflare | Yes (since 2018) | Yes | Honours both response and request directives |
| Fastly | Yes (via Surrogate-Control or Cache-Control) | Yes | Stale-on-error more aggressive |
| CloudFront | Yes (since 2022) | Yes | Stale-on-error needs origin error caching policy |
| Varnish | Yes (grace keyword in VCL) | Yes (stale-if-error) | See varnish-test-vtc-syntax |
| nginx | Yes (proxy_cache_use_stale updating) | Yes (proxy_cache_use_stale error timeout) | Different config keyword |
| Chrome/Firefox | Yes (browser cache honours SWR/SIE) | Yes | Per-tab behaviour may surprise; test |
| Service Workers | Manual implementation in code | n/a | Workbox provides a SWR strategy |
Testable behaviours
| Behaviour | Test |
|---|---|
| SWR serves stale within window | Set max-age=1, SWR=300; wait 5s; request → stale served + async revalidate |
| SWR triggers revalidation | Verify origin sees one revalidate request after the user's request returned |
| SWR window enforced | Wait > max-age + SWR; next request blocks origin |
| SIE serves stale on origin 5xx | Take origin down; request within SIE window → 200 with stale data + warning header (RFC 7234 Warning header may be present) |
| SIE window enforced | Origin down beyond SIE window → user sees 5xx |
| SWR + must-revalidate doesn't surface stale | Verify must-revalidate wins |
| Stampede mitigation under load | N=1000 concurrent at t=max-age+1s → origin sees 1-2 revalidates, not 1000 |
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Cache-Control: must-revalidate, stale-while-revalidate=300 | Contradictory; must-revalidate wins, SWR silently ignored | Drop must-revalidate |
stale-while-revalidate on private user data | Stale for one user could expose old data to that same user | Be deliberate; pair with private |
| SWR=0 | No grace period; equivalent to omitting | Use ≥30s |
| SWR window > max-age * 10 | Stale data shown for an excessive fraction of total lifetime | Keep proportionate |
| SIE without alarm on origin 5xx rate | "Site looks fine" but origin down for days | Pair SIE with monitoring |
| Per-page Cache-Control inconsistent (some SWR, some not) | Confusing UX during partial outages | Codify SWR policy per response class |
| Browser ignores SWR (older browsers) | Polyfill via Service Worker for critical paths | Test with target browser matrix |
Limitations
References
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-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.
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.
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.