System Design Reliability
Graceful Degradation
Black Friday checkout is humming. Recommendations lag, the personalization service tips over, and CPU on the survivors climbs past 90%. Users still need to pay. They do not need a perfect “for you” rail.
is deciding that swap before the page: flip a , climb a , and low-value work so auth and payment stay up.
Degradation ladder
Rank what never drops vs what can go stale.
Disable personalization in seconds.
Fresh → stale → static → honest wait.
Drop P3 work before checkout.
Try it: climb the degradation ladder with feature flags
Black Friday load rises while a personalization dependency browns out. Turn flags off in priority order so checkout keeps working even when nice-to-have surfaces go stale or static.
- Utilization
- 236%
- Ladder step
- Critical
- Checkout
- At risk
- Optional load
- 1750 rps
Recommendations rail
P3Costs ~400 rps when enabled. Tier P3 drops before checkout.
Personalized ranking
P2Costs ~600 rps when enabled. Tier P2 drops before checkout.
Live inventory counts
P2Costs ~500 rps when enabled. Tier P2 drops before checkout.
Real-time analytics beacons
P3Costs ~250 rps when enabled. Tier P3 drops before checkout.
Critical · essential checkout only
Raise load or disable feature flags manually. Checkout stays tier P0; lower tiers drop first so payment keeps working under pressure.
Decide before the incident. Feature flags encode the ladder ahead of time so responders are not debating product policy while CPU is pegged.
Fallback chains beat hard failures. Live inventory may become yesterday's cached count with a banner. That is preferable to blocking payment when a read model is slow.
What actually happens
Overload and dependency brownouts are normal at scale. Without a plan, every service competes for the last CPU cycle and the whole site returns 500s. With a plan, you climb a ladder: personalized → cached → simplified → → queued → shed → essential only.
Automation and make those steps deliberate. A opens on a sick dependency and walks a so quality drops smoothly. Users see a banner for stale data; responders are not inventing policy at 3am.
Exit rules matter as much as entry. Use so the system does not flap between degraded and normal every few seconds under noisy load.
How to choose what to degrade
1. What is the product promise? For commerce, checkout beats recommendations. For messaging, send/receive beats avatar decoration. Name that promise first.
2. What scarce resource is on fire? DB write saturation, cache outage, and external API failure each want a different first cut: see the table later.
3. Can users understand the trade? Stale data needs labels, queued writes need receipts, and rejected work needs clear retry guidance, not silent failure.
Simple rule of thumb
Prefer a reduced experience over total failure. Pre-classify every feature by , wire flags and breakers before incidents, and climb back only after a healthy window.
In simple terms: plan the boring mode of your product. That is the mode that saves the business day.
The degradation ladder
Plan a ladder from rich experience to basic service. Each step should be deliberate, reversible, observable, and understandable to users.
Normal: personalized, fresh, full fidelity
Cached: stale-while-revalidate
Simplified: cheap model / smaller payload
Read-only: protect write path
Queue: accept work asynchronously
Shed: reject low-priority traffic
Essential only: auth, checkout, health
Trigger
Choose a trigger tied to the resource that is actually constrained. If database connections are saturated, disabling a CPU-heavy animation service will not release them; pausing expensive database-backed recommendations may. Require the signal to persist for a defined interval and record which rule activated the mode. Keep a manual override for incidents that automation cannot classify reliably.
User signal
Show a small banner for stale data, queued writes, or temporary feature limits.
Exit rule
Climb back only after metrics are healthy long enough to avoid flapping. That is . For example, enter a reduced mode when queue age stays above 30 seconds for two minutes, but return to normal only after it stays below 10 seconds for five minutes. Different entry and exit thresholds create hysteresis, preventing noisy measurements from repeatedly toggling the feature. Increase restored traffic gradually because recovery itself can refill caches and create fresh load.
Degradation playbooks
A playbook converts "degrade gracefully" into exact triggers, actions, user messages, owners, exit rules, and metrics.
| Incident signal | Trigger | Action | User-visible behavior | Exit rule |
|---|---|---|---|---|
| Database write latency high | P99 write latency above target for 5 min or queue age high. | Disable non-critical writes; queue low-priority writes with receipts. | Some updates may take a few minutes. | Latency healthy for 15 min and backlog below threshold. |
| Cache outage | Cache hit rate drops below target and database load rises. | Disable personalization and wide fanout reads. | Popular or stale results are shown. | Cache warmup complete and DB load below threshold. |
| Surviving region overloaded | Utilization above 90% after failover. | Shed background work, cap retries, lower media quality. | Core flows continue with reduced quality. | Utilization below 85% for the recovery window. |
Feature flags for controlled degradation
flip functionality without redeploying binaries: perfect for instantly disabling risky widgets when metrics spike or dependencies lag.
Feature degradation contract
- What is disabled or reduced.
- Who can flip the flag.
- Which metric triggers it.
- What users see.
- How to verify flag propagation.
- How to recover without flapping.
Tier 0: Critical
Auth, payment, core reads. Never degrade.
Tier 1: Important
Recommendations → popular. Notifications → queue. Search → basic.
Tier 2: Nice-to-have
Avatars → placeholder. Feeds → 5-min stale. Analytics → drop.
Tier 3: Expendable
A/B telemetry, social, background jobs. Disable first.
if (featureFlags.isEnabled("recommendations")) {
return recommendationService.getPersonalized(userId)
}
return fallbackService.getPopularItems() // Static cache
// Circuit breaker triggers flag. Propagation: < 5s across 10K serversStatic fallbacks and cache-only mode
Serving last-known-good HTML or CDN assets buys minutes to hours while databases recover. Users notice staleness but stay productive versus staring at 500s: often via . A stale response is acceptable only within the data’s correctness contract. An old catalog description may still be useful, while a cached permission decision or inventory quantity can authorize an invalid action. Mark freshness when it affects decisions and revalidate critical state at the write boundary. Ensure the fallback can be served without contacting the same failed dependency it is meant to replace.
Static fallback
Pre-cache homepage (hourly), product (daily), recommendations (top-100). CDN + disk + memory. Backend dead = users still see content.
Cache-only mode
Normal: cache miss → service → DB. Degraded: cache hit or stale with a "data may be outdated" banner.
When writes are overloaded, keep safe reads available and accept deferred mutations only if the product supports pending work. A successful acceptance response must mean the command is durably recorded with an operation ID, not merely buffered in process memory. Show “queued” and later completion or failure. For operations such as reserving the last seat, rejecting promptly may be safer than promising a write that cannot yet be validated.
Queue-based load leveling
Queues absorb bursts: clients enqueue work instead of hammering synchronous endpoints, but backlog grows if consumers lag. Monitor depth and age alongside enqueue latency, and apply when the queue is no longer useful.
A queue absorbs a temporary excess of arrivals over processing capacity. If traffic reaches 100,000 jobs/s for ten seconds while workers handle 10,000 jobs/s, backlog grows by 900,000 jobs, assuming all are durably accepted. If arrivals then stop, draining takes 90 seconds; if arrivals remain at 10,000 jobs/s, there is no spare capacity to drain it. Bound acceptance by useful waiting time and storage.
Client → API (enqueue) → "Processing..." | Durable queue | Workers → DB
depth < 1K
Normal, < 1s
depth < 100K
Elevated, < 2 min
depth > 100K
Critical: enable shedding
Queue backlog math
Arrival rate 100K/s and worker capacity 40K/s means backlog grows by 60K/s. If work expires after 5 minutes, max useful backlog is 12M items. Beyond that, shed work instead of accepting false promises.
After a 10-minute surge, backlog is 36M items. At 40K/s drain with no new arrivals, drain time is 15 minutes; with normal traffic continuing, drain time may be much longer.
Backpressure tells producers that the system cannot safely accept more work. Use a queue-age or projected-wait threshold alongside queue depth, because a thousand slow jobs can be worse than a million tiny ones. Reject with retry guidance before the waiting time exceeds the job’s value. Expire only work whose semantics allow it, and record an explicit failure for accepted commands that require a final outcome.
Choosing what to degrade under pressure
Degrade the work that consumes scarce resources without protecting the main promise of the product.
| Pressure | Degrade first | Protect | Tradeoff |
|---|---|---|---|
| DB write saturation | Non-critical writes, analytics, profile edits. | Orders, payments, identity changes. | Some actions queue or become read-only. |
| Cache outage | Expensive personalization and wide fanout reads. | Small critical reads with rate limits. | Users see popular or stale content. |
| External API failure | Live enrichment, fraud score refresh, shipping estimate refresh. | Previously authorized flows and safe defaults. | Lower freshness, higher manual review later. |
Million-user degraded modes
A dependency outage at million-user scale should turn into lower-quality answers, not a blank page. Predefine the fallback for each expensive feature.
Dependency outage or survivor capacity > 90%
Feature tier
Recommendations
Popular by category + stale cache
Show degraded banner
Search
Basic lexical index + cached filters
Show degraded banner
Media
Lower bitrate, thumbnails only, delayed uploads
Show degraded banner
Core account
Protect login, reads, writes
Recommendations outage
Skip personalization, serve popular/category lists from cache, and cap refresh frequency to avoid stampedes.
Search outage
Use last-built index, simpler ranking, fewer filters, and explicit "results may be limited" messaging.
Media pressure
Lower bitrate, defer transcodes, pause previews, and queue uploads with receipts instead of failing the whole flow.
Priority-based load shedding
When CPUs saturate, drops lowest-value requests first: analytics, previews, heavy personalization. So carts and authentication stay responsive under siege.
Fairness and isolation rules
- Reserve capacity for critical user journeys.
- Cap heavy tenants, expensive queries, and bulk exports.
- Shed background and analytics work before interactive traffic.
- Return retry guidance for retryable requests so clients do not stampede.
P0 Critical
Payment, auth, health
P1 High
Product, search, cart
P2 Medium
Recs, reviews
P3 Low
Analytics, webhooks
| Load signal | What to shed |
|---|---|
| CPU > 80% | Disable or queue P3 (analytics, webhooks). |
| CPU > 90% | Also shed P2; keep product-critical paths. |
| CPU > 95% | Run P0 only (payments, auth, health): return 503 with Retry-After elsewhere. |
Pair shedding with client backoff; golden-path checkout staying up under stress is the usual product bar.
Circuit breaker with fallback chains
stop hammering sick dependencies; chain multiple fallbacks (fresh → stale cache → static) so each hop degrades quality smoothly instead of failing closed immediately. Keep fallback chains short and cheaper at each step. If personalized ranking times out, a precomputed popular list can avoid the failed ranking service entirely. If that list is unavailable, a static response may preserve navigation. Give the whole chain one deadline; assigning a full timeout to every fallback can make degraded mode slower than the original failure.
Fallback chain
- Primary: ML recs (P99 50ms)
- Fallback 1: Simplified model (5ms)
- Fallback 2: Category popular (cached, 1ms)
- Fallback 3: Global top-100 (CDN)
- Fallback 4: "Check back later"
Media-service fallback
1. Personalized results 2. Daily cached results 3. Top trending from static object storage 4. Editorial fallback. Users still see usable content.
Degradation strategy patterns
Pattern-match to your domain: pre-ranked services, ruthless protection of media/checkout paths, and manual toggles exercised quarterly.
Media-first product
Every service ranked by criticality. Non-critical paths throttle first. Personalization, social, and analytics degrade before playback. Video remains last standing.
Commerce peak event
Pre-plan tiers weeks ahead. Recommendations, recently viewed items, and reviews are shedable. Incident room toggles flags. Checkout is protected while homepage personalization can be disabled to free payment capacity.
How to talk about degradation tradeoffs
- Lead with product priorities: name which features are P0, P1, P2, and why.
- Connect degradation to reliability controls: circuit breaker opens, feature flag flips, cache-only mode starts, and clients receive
Retry-After. - Call out user trust: stale data needs labels, queued writes need receipts, and rejected work needs clear retry guidance.
- Explain recovery: use and health windows so systems do not flap between degraded and normal modes.
Key takeaway
At scale, total availability is a myth: partial degradation is the reality you design for. Pre-classify features by criticality, build fallback chains from rich to basic, use feature flags and circuit breakers, and shed load with priority so critical paths survive. Users tolerate degraded features far better than total outages. Plan those paths before the page.
Deepening: feature degradation contracts
For each non-critical feature, define entry and exit conditions, user messaging, correctness boundaries, and fairness under shed load. Automate degraded-mode tests so the fallback is not imaginary.
Primary sources
- Google SRE: Handling Overload: load shedding and degraded modes.
- Google SRE: Addressing Cascading Failures: failing open/closed safely.
- microservices.io: Bulkhead: isolation so degradation stays local.
Practice and further reading
Use these exercises to connect the lesson to a design decision. State your assumptions, trace what happens to one request, and explain the rule that must remain true even during a failure. That rule is the design's correctness invariant.
Focus for this lesson: Define per-feature degradation contracts, correctness boundaries, fair shedding, user messaging, entry/exit gates, and continuous degraded-mode tests.
Explain the decision
What should degrade first in an e-commerce checkout?
Read the reasoning
Recommendations, reviews, and rich media before inventory, price, identity, payment correctness, or order confirmation.
Apply it to a production scenario
Search is overloaded. How do you preserve a useful product?
Read one approach
Reduce facets/personalization, serve cached popular results, cap expensive queries, shed anonymous traffic fairly, and show honest freshness.
Work through the numbers
Dropping a personalization stage saves 45 ms and 25% CPU from a 180 ms path. What is degraded latency?
Read the worked calculation
About 135 ms if stages are serial, while capacity benefit must be load-tested rather than inferred solely from CPU.
Reason through a failure
What must degraded mode never do?
Read the recovery reasoning
It must not fabricate success, bypass security, violate money/inventory invariants, or return stale data beyond the stated contract.
Sources and further study
Continue with Authentication & Authorization.