CDN Network Resilience: Circuit Breakers, Retries, and Graceful Degradation
A resilient CDN doesn't just accelerate content — it absorbs failure. When an origin hiccups, a PoP goes dark, or a network path degrades, users shouldn't see it. The difference between a minor blip and a global outage is how you design for failure: circuit breakers to stop cascades, smart retries to handle transients, and graceful degradation to keep serving value when perfect is impossible.
For teams new to edge design, start with CDN fundamentals and how a CDN works to ground the concepts below. If you are scaling commerce workloads, resilience directly impacts conversion — see our guide to CDN for ecommerce.
1. Why Resilience Matters at the Edge
CDNs operate as a distributed mesh of 200+ PoPs, each handling thousands of concurrent origin fetches. Unlike a single data center, failure is not an exception — it's a constant. Fiber cuts, BGP churn, origin autoscaling delays, and TLS handshake timeouts happen every day.
Without resilience controls, a slow origin triggers a classic cascade: edge servers hold connections open waiting for origin, thread pools saturate, health checks fail, the load balancer shifts traffic to the next PoP, which then saturates too. Within 90 seconds, a regional slowdown becomes a global 502 storm.
Resilient design inverts this. Instead of trying to prevent all failures, you isolate them, limit their blast radius, and degrade intentionally. The three primitives that achieve this are:
- Circuit breakers — stop calling a failing dependency.
- Retries with backoff — recover from transient faults without amplifying load.
- Graceful degradation — serve a useful, if imperfect, response when the ideal path is unavailable.
Design principle: Every edge-to-origin call should have a timeout, a retry budget, a circuit breaker, and a fallback. If any one is missing, you have a single point of failure.
2. Circuit Breakers: Preventing Cascading Failures
A circuit breaker wraps an origin or service call and tracks its health. Like an electrical breaker, it trips when failure exceeds a threshold, immediately failing fast instead of waiting for timeouts.
| Metric | Edge Processing | Origin Processing | Improvement |
|---|---|---|---|
| Latency (p50) | 8–15 ms | 120–300 ms | 15–20x faster |
| Cache Hit Ratio | 85–95% | N/A | Reduces origin load |
| TLS Handshake | 1–5 ms (nearby PoP) | 50–150 ms | 10–30x faster |
| Bandwidth Cost | $0.01–0.04/GB | $0.05–0.12/GB | 3–5x cheaper |
| Availability | 99.99% (multi-PoP) | 99.9% (single DC) | 10x fewer outages |
How it works in a CDN context:
- Closed: Requests flow to origin. A sliding window (e.g., 60s) counts errors. If 50% of 100 requests fail or latency p95 exceeds 2s, trip to open.
- Open: For a cooldown (e.g., 30s), edge returns immediately without contacting origin. This protects origin from thundering herd and frees edge workers. Serve from cache with stale-if-error or a static fallback.
- Half-Open: After cooldown, allow N probe requests (e.g., 5 per second). If success rate recovers above 80%, close the circuit. Otherwise, reopen.
Implement breakers per origin, per PoP, and per route tier. Your checkout API deserves a tighter threshold than static assets. For deeper edge logic patterns, see edge computing guide.
Configuration Example (Edge Worker)
const breaker = new CircuitBreaker({
failureThreshold: 0.5, // trip if 50% fail
minimumRequests: 50, // within window
windowMs: 60_000,
cooldownMs: 30_000,
halfOpenMaxCalls: 5
});
async function fetchOrigin(req: Request): Promise<Response> {
if (breaker.isOpen()) {
const stale = await caches.default.match(req);
if (stale) return stale; // graceful stale serve
return new Response("Service temporarily degraded", { status: 503 });
}
try {
const res = await fetch(req, { signal: AbortSignal.timeout(3000) });
if (res.status >= 500) throw new Error(`origin ${res.status}`);
breaker.recordSuccess();
return res;
} catch (err) {
breaker.recordFailure();
throw err;
}
}
Key details: enforce a 2-4 second origin timeout (never rely on default 30s), and record both network errors and 5xx as failures. 4xx should not trip the breaker.
3. Intelligent Retry Strategies
Retries fix transients, but naive retries amplify outages. The goal is to retry only when it helps, and never more than the system can absorb.
| Strategy | When to Use | Backoff | Risk if Misused | CDN Best Practice |
|---|---|---|---|---|
| No Retry | Non-idempotent POST, 4xx | — | None | Default for mutations |
| Immediate Retry | Alternate PoP / origin | 0 ms, different target | Doubles load | Max 1, only on 502/503/504 |
| Exponential + Jitter | Transient 5xx, timeouts | 100ms × 2^n + jitter | Tail latency spike | Max 2 attempts, cap 2s |
| Hedged Request | High p95 latency | Send 2nd after 300ms | 2× origin load | Enable for <10% traffic |
Rules for Safe Retries at the Edge
- Bound the budget: Global retry budget of 20% of requests. If exceeded, stop retrying.
- Retry only safe conditions: Network errors, 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout. Never retry 500 without understanding origin semantics, and never retry 429 without respecting Retry-After.
- Idempotency first: Only auto-retry GET, HEAD, OPTIONS. For POST, require an Idempotency-Key.
- Jitter is mandatory: Without jitter, all edges retry in lockstep and create a spike. Full jitter: sleep = random(0, base * 2^n).
- Prefer failover over retry: Retry to a different origin or PoP, not the same failing node.
sub vcl_fetch {
if (beresp.status >= 500 && beresp.status < 600) {
if (req.restarts < 1 && bereq.is_bgfetch == false) {
return (restart); // one failover retry
}
# Graceful: serve stale if available
if (stale.exists) {
return (deliver_stale);
}
}
# Cache stale controls
set beresp.stale_if_error = 86400s;
set beresp.stale_while_revalidate = 60s;
}
Combine retries with your cache invalidation strategy: a stale object from 5 minutes ago is often preferable to a 503 during a purge storm. Set stale-while-revalidate and stale-if-error headers at origin so edges know how long stale is acceptable.
4. Graceful Degradation Patterns
When the circuit is open and retries are exhausted, you still have options. Graceful degradation means delivering reduced functionality rather than no functionality.
Pattern 1: Tiered Content Serving
Define degradation tiers in advance:
- Tier 0 (Full): Personalized, dynamic, fresh from origin.
- Tier 1 (Stale): Cached version up to N hours old. For most users, indistinguishable.
- Tier 2 (Static): Pre-generated static shell or placeholder (e.g., “Recommendations unavailable”).
- Tier 3 (Minimal): Lightweight HTML with no JS, served from edge KV.
For ecommerce, Tier 1 might mean showing a product page with a 10-minute-old price and an “availability may vary” banner — far better than a checkout error. Pair this with proper TLS configuration so degraded responses don't trigger mixed-content or certificate errors.
Pattern 2: Feature Flags at the Edge
Use edge KV or config to disable expensive features under stress. Example: if origin latency >800ms, disable real-time inventory check and show cached stock, disable personalization API, and load a lighter image variant.
const DEGRADED = await EDGE_CONFIG.get("degraded_mode"); // "off" | "stale" | "static"
if (DEGRADED === "stale") {
const cached = await caches.match(request);
if (cached) return cached;
}
if (DEGRADED === "static") {
return fetch("/fallbacks/product-shell.html");
}
// otherwise, normal origin fetch with breaker + retry
Pattern 3: Load Shedding and Prioritization
Not all requests are equal. Under duress, shed low-value traffic first: bots, prefetch, non-critical API polling. Prioritize checkout, login, and cache misses for HTML over background revalidation. Implement concurrency limits per origin (e.g., max 500 in-flight fetches per PoP) and queue or fast-fail excess with 503 + Retry-After: 5.
5. Observability and Recovery
Resilience without visibility is guesswork. Instrument everything:
- Circuit events: Log every open/half-open/close transition with PoP, origin, and trigger reason. Alert on flapping (more than 3 trips in 10 minutes).
- Retry metrics: Track retry count, success rate, and hedged request waste. If retry success <10%, your retries are hurting.
- Stale serve ratio: Percentage of responses served stale. A spike means origin trouble even if 5xx stays low.
- Origin concurrency and queue depth: Early warning before timeouts cascade.
Recovery should be automatic but deliberate. Half-open probing prevents the classic “recovery thundering herd” where all PoPs simultaneously hammer a recovering origin. Use exponential cooldown: 30s, 60s, 120s on repeated failures, and require sustained success before closing.
Run chaos experiments: inject 2-second origin latency in one region, blackhole an origin for 60 seconds, or expire cache globally. Verify that circuits trip, stale serves, and users see Tier 1 content — not errors.
6. Putting It Together: Resilient Architecture Checklist
Before your next peak event, verify:
- Every origin fetch has a 2-4s timeout and per-route circuit breaker (50% failure, 60s window, 30s cooldown).
- Retries limited to 1 failover retry for GET 502/503/504 with full jitter and 20% global budget.
- Cache-Control: stale-while-revalidate=60, stale-if-error=86400 set for cacheable assets.
- Tiered fallbacks defined and tested — stale, static shell, and minimal edge page.
- Feature flags controllable from edge config in <5 seconds.
- Dashboards for circuit state, stale ratio, and origin concurrency; alerts on trip and flapping.
Resilience is not a single feature but a set of habits. Circuit breakers give you time, retries give you a second chance, and graceful degradation gives you a way to keep your promise to users when the ideal path is gone. Design for failure, and your CDN will feel invisible — exactly as it should.
Frequently Asked Questions
What is the difference between a retry and a circuit breaker in a CDN?
A retry re-attempts a failed request to the same or alternate origin, useful for transient errors like a single 502. A circuit breaker stops further attempts for a cooldown after repeated failures, preventing overload. Use retries for isolated blips and circuit breakers for sustained degradation.
When should a CDN serve stale content instead of failing?
Serve stale on 5xx, timeouts, or when the circuit is open, if the cached object is within your stale-while-revalidate or stale-if-error window. It's ideal for cacheable GETs — images, CSS/JS, and product pages — where slightly outdated content is better than an error.
How do you avoid retry storms at the edge?
Limit to 1-2 retries per request, use exponential backoff with jitter, retry only safe methods and status codes (502/503/504), enforce a global retry budget, and combine with circuit breakers. Prefer failover to a different origin rather than hammering the same one.
What metrics indicate you need graceful degradation?
Watch origin p95 latency, 5xx rate, circuit state transitions, cache hit ratio, and edge-to-origin concurrency. If p95 latency doubles or 5xx exceeds 1-2% for 60 seconds, trigger degraded modes like stale serving, disabling personalization, or serving static shells.