At 2:17 AM on a Black Friday in 2019, I watched our primary origin cluster go completely dark during the biggest traffic spike of the year. The database had locked up, health checks were failing, and 40 million shoppers were about to see a blank screen. What saved us wasn't a heroic database fix — it was a stale cache rule we'd enabled three weeks earlier that kept serving product pages for the next 47 minutes while we recovered. If you've never had that moment of panic, you haven't run a CDN at scale.
Stale Cache Serving: Your First Line of Defense When Origins Disappear
Most teams think of cache as a performance optimization, but in reliability engineering, it's your cheapest insurance policy. When your origin returns a 500, 502, or just times out, a properly configured CDN doesn't have to pass that error to the user. It can serve the last good version it has on disk or in memory. I've seen this single capability turn what would've been a severity-1 outage into a minor blip that barely registered on our error dashboards. The trick is telling the CDN how long that stale content is still safe to use.
That behavior is controlled by two HTTP Cache-Control extensions that are now standardized: stale-while-revalidate and stale-if-error. The HTTP Working Group Specifications define them clearly, but here's how they work in practice. Stale-while-revalidate lets the CDN serve the cached copy immediately while it fetches a fresh one in the background, so users never wait. Stale-if-error is your emergency brake — it says "if the origin is broken, you're allowed to keep serving this old response for X more seconds." Without these directives, most CDNs will honor max-age strictly and then fail open to the error. For a deeper look at how this fits into overall reliability culture, I often point teams to SRE Practices for CDN: Applying Site Reliability Engineering to Content Delivery because fallback strategies aren't just config, they're operational commitments.
We typically set max-age short for freshness, but we give ourselves a huge safety net on the error side. For our product catalog, we use a five-minute fresh window and allow stale-if-error for a full day. That doesn't mean we serve day-old prices — our purge pipeline invalidates on stock and price changes — but it means a short origin outage won't empty the edge cache instantly. The balance is always freshness versus availability, and you have to decide it per content type.
# Origin response headers for a product page with stale fallback
Cache-Control: public, max-age=300, stale-while-revalidate=60, stale-if-error=86400
CDN-Cache-Control: max-age=300, stale-while-revalidate=60, stale-if-error=86400
Surrogate-Key: product-88421 category-shoes
# Fastly / Varnish VCL fallback logic for stale content delivery
sub vcl_hit {
if (obj.ttl >= 0) {
return (deliver);
}
if (std.healthy(req.backend_hint)) {
if (obj.ttl + obj.grace > 0) {
return (deliver);
}
} else {
# Origin is unhealthy, serve stale for up to 24h
if (obj.ttl + obj.grace > 0 && obj.ttl + obj.grace < 86400) {
return (deliver);
}
}
}
sub vcl_error {
if (obj.hits > 0) {
return (deliver);
}
}
Static Fallback Pages That Don't Feel Like an Outage
Stale cache is fantastic until you get a cache miss during an outage. A new URL, a purged object, or an edge node that never saw the content before has nothing stale to serve. That's when you need your second layer: static fallback. This isn't your generic "Origin Unreachable" nginx page. A good static fallback is a pre-built, fully functional shell of your site — often a simplified homepage, category listings, or even a read-only version of the app — stored directly on the edge or in object storage like S3 and served when everything else fails.
Back in 2021, after a DNS provider issue took our origins offline globally, we realized our error page was just a white page with "500" on it. It was technically correct but it killed trust. We rebuilt our fallback as a static site generated every 15 minutes with the top 1,000 products, search still working via a client-side index, and a clear banner saying "Live updates paused — check back shortly." Bounce rates during the next incident dropped from 92% to 34%. If you're weighing how much effort to put into this, check your error budgets first. As discussed in CDN Error Budget Management: Balancing Reliability with Feature Velocity, a beautiful fallback that burns two sprints might not be justified if you're already well within your SLO, but a bad fallback will burn trust you can't measure.
The implementation matters less than the deployment pattern. You want the fallback to have zero dependencies on your origin. We push our static bundle to every CDN edge via Workers KV and keep a copy in R2/S3 with a failover route. The guidance in web.dev Articles on offline and resilience patterns is surprisingly useful here, especially for making that static shell feel fast with precached assets and service workers. CDN error handling should always prefer a useful, slightly outdated page over a technically accurate error code.
// Cloudflare Worker: static fallback with stale cache tiering
export default {
async fetch(request, env, ctx) {
const cache = caches.default;
let response = await fetch(request);
// If origin succeeds, cache and return
if (response.ok) {
ctx.waitUntil(cache.put(request, response.clone()));
return response;
}
// Try stale cache first
let cached = await cache.match(request);
if (cached) {
cached = new Response(cached.body, cached);
cached.headers.set('X-Cache-Status', 'STALE');
cached.headers.set('X-Fallback', 'stale-cache');
return cached;
}
// Cache miss + origin failure - serve static fallback from KV/R2
const fallback = await env.STATIC_FALLBACK.get("index.html", "stream");
if (fallback) {
return new Response(fallback, {
status: 200,
headers: {
"Content-Type": "text/html",
"X-Fallback": "static-shell",
"Cache-Control": "public, max-age=30"
}
});
}
// Last resort: custom error JSON/HTML
return new Response(JSON.stringify({ error: "Service temporarily unavailable" }), {
status: 503,
headers: { "Content-Type": "application/json", "Retry-After": "60" }
});
}
}
Custom Error Responses and How Fallback Strategies Actually Compare
Even with stale cache and static fallbacks, you'll still need to handle the cases where you intentionally want to show an error — but you get to control what that error looks like. Default CDN error pages are terrible for brand and debugging. We've moved to custom error handling where every 5xx from origin is intercepted at the edge, logged with a request ID, and replaced with a user-friendly, on-brand page that still returns the correct status code for SEO and monitoring. The difference is we don't expose stack traces or "Varnish Error" banners, and we include a Retry-After header so browsers and search crawlers know to back off.
Choosing between these fallback strategies isn't about picking one winner — it's about layering them by cost and user impact. Stale cache is nearly free and instantaneous, static fallbacks cost storage and a build pipeline but cover cache misses, and custom errors are your safety net when you have nothing else to show. I usually map this out with product and engineering together so we don't over-engineer the least-used path. Much of this operational decision-making becomes easier when you've reduced manual intervention, which is why I recommend reading CDN Toil Reduction Through Automation: Eliminating Repetitive Operations Work before you automate your fallback pushes.
To make the trade-offs concrete, here are numbers from a mid-sized e-commerce platform I advised last year handling about 12,000 requests per second at peak. We A/B tested each fallback under simulated origin failure (origin latency injected to 5s then 100% 500s) and measured from the edge POPs in North America and Europe.
| Fallback Strategy | Avg. TTFB During Outage | Cache Hit / Serve Rate | User-Perceived Availability | Freshness Lag |
|---|---|---|---|---|
| Stale Cache (stale-if-error=86400) | 45 ms | 88% of requests served stale | Putting It Together: Practical Implementation at the Edge
Knowing the theory behind stale serving and static fallbacks is one thing - wiring it up so it actually fires when your origin's on fire is where most teams stumble. You don't want to discover your fallback logic was misconfigured during a real outage. In my experience, the cleanest pattern is to treat the edge as the decision maker, not the origin. That means your cache-control headers, grace windows, and error handling all live in VCL, Workers, or your CDN's edge rules, with the origin just declaring how long content is fresh. For most stacks, I recommend starting with a 60s TTL for dynamic HTML, a 24h grace window for
Advanced Patterns and Troubleshooting When Fallbacks Don't FireYou've deployed the logic, but fallbacks silently fail more often than you'd think. The most common culprit we see is cache key mismatch. Your Worker is looking for For high-traffic properties, you’ll want tiered shielding plus active health probing, not just passive error detection. Passive detection waits for a user request to fail, which costs you 5 seconds per user. Active probing hits If you're still seeing 503s despite stale being available, check three things in order: 1) Are you sending Frequently Asked QuestionsHow long should I keep stale content before it’s too old to serve?For most content sites, 24 hours is the sweet spot, 48 hours max for static articles. For e-commerce or highly dynamic pages like pricing or inventory, cap it at 1-2 hours - serving a stale "in stock" page for a day will create more support tickets than a branded 503. Set Will serving stale content hurt my SEO?Not if you do it correctly. Search crawlers respect 503 + What's the difference between stale-while-revalidate and stale-if-error?Think of them as two different triggers. How do I test fallback strategies without causing a real outage?Don't wait for production to fail. Three safe methods: 1) Use your CDN's edge rule to inject a 500 for a specific test header or query param like External ReferencesRelated Articles
References & Standards: HTTP Working Group Specifications · web.dev Articles · IETF Datatracker |