Hermes Wiki

Graceful Degradation

Concept

Graceful degradation is the design discipline of keeping a system's core functionality available when a dependency, or the system itself, is under stress or partially failing — rather than letting one broken piece take down the whole request, or the whole service. AWS's Well-Architected Reliability Pillar frames the underlying move precisely: turn a hard dependency (one whose failure makes the whole feature/request fail) into a soft dependency (one whose failure degrades the experience but doesn't break it) wherever the dependency isn't actually essential to the request's core value. A product page that hard-fails when the "customers also bought" recommendation service is down has an unnecessary hard dependency; one that just omits the recommendations and serves the rest of the page has turned it soft.

Graceful degradation isn't one mechanism — it's a composition of other resilience patterns aimed at a specific outcome: circuit breakers and bulkheads stop a failing dependency from taking down the caller's own resources; retries with backoff handle transient blips; feature flags / kill switches let an operator (or an automated trigger) instantly disable a non-essential feature or fall back to a cached/default response without a deploy; and load shedding — deliberately rejecting or simplifying a fraction of incoming requests under extreme load — keeps the system alive for the traffic it does accept rather than collapsing under all of it. Graceful degradation is what these mechanisms are for; none of them alone constitutes it.

Tradeoffs

Strategy Benefit Cost
Fail-fast on any dependency failure (no degradation) Simplest to build and reason about; failure is obvious and immediate Any dependency's failure — even a genuinely optional one — takes the whole feature or request down; the blast radius is as wide as the least-reliable dependency in the call chain
Soft dependencies + cached/default fallback Core functionality survives a dependency outage; often invisible to the end user Every fallback path is code that only runs during an incident — the highest-risk time to discover it's broken or stale; needs its own testing discipline (chaos engineering, game days), not just happy-path coverage
Feature flags / kill switches Fast, no-deploy mitigation during an active incident; reversible instantly once the dependency recovers Adds runtime branching complexity and a flag inventory to maintain; a flag left in the wrong state after an incident is a silent, easy-to-forget outage risk
Load shedding under overload Preserves service for the traffic it does accept instead of every request timing out together Requires classifying requests by priority ahead of time (which traffic gets shed first) — done reactively during an incident, it's guesswork instead of policy

The common thread: every degradation strategy trades build/maintenance complexity now for a narrower blast radius later. A system with zero soft dependencies is the cheapest to build and the most fragile in production; a system with graceful fallbacks everywhere is more code to write and test, in exchange for dependency failures becoming inconvenient instead of catastrophic.

When to use / when not to

  • Turn a dependency soft whenever its failure doesn't need to block the request's core value — a recommendation widget, a non-blocking analytics call, an optional enrichment lookup are classic candidates.
  • Prioritize load shedding at any true fan-in point (a shared API gateway, a single overloaded database) where the alternative is every request timing out together rather than most requests succeeding.
  • Wire feature flags around any dependency that's had a real incident before, or one operated by a third party outside your control — the value of a kill switch is proportional to how likely you are to need it fast, under pressure, without a deploy pipeline in the loop.
  • Don't build fallback paths for dependencies that are genuinely core to the request's value — degrading a payment charge into "silently skip it" isn't graceful degradation, it's a correctness bug wearing a resilience label. Some dependencies are legitimately hard, and the honest answer there is investing in their reliability, not faking a fallback.

Common pitfall

Building a fallback path and never exercising it until the incident that needs it — at which point it's just as likely to be broken as the primary path was, because it's the least-tested code in the system by construction. AWS's own framing on this is blunt: they explicitly prefer consistent, predictable performance over letting a degraded path silently add latency, because added latency during an overload scenario tends to ripple into other systems rather than staying contained — a poorly designed "graceful" fallback can make an incident worse, not better. The fix is treating fallback paths as first-class code paths: exercised in game days/chaos testing, monitored separately from the primary path, and alerted on when they're the one actually serving traffic (a fallback silently serving 100% of requests for a week because nobody noticed the primary broke is not graceful — it's a hidden outage).

Engineering Lens

The design-review-worthy version of this pattern isn't "we have fallbacks" — it's a legible answer to "for each dependency this service calls, is it hard or soft, and if soft, what specifically happens when it's gone?" That answer should be a deliberate classification made at design time, not something inferred from what happened to work during the last incident. The same discipline that decides which Trust Services Criteria matter for a SOC 2 scope, or which resource pools need bulkhead isolation, applies here: enumerate every dependency, classify its criticality honestly, and design the failure behavior for the non-critical ones before production forces the question — because during an actual outage is the worst possible time to be deciding, for the first time, whether a dependency was ever supposed to be optional.

Sources

Hermes Wiki