Fault Tolerance, High Availability, and Disaster Recovery Questions
Keeping a system serving despite failure, from code-level resilience to infrastructure-level recovery: circuit breakers, retries with backoff and jitter, timeouts, bulkheads, graceful degradation, and preventing cascading failures, alongside redundancy, failover (active-active versus active-passive), RPO and RTO objectives, backup and restore, and multi-region failover. Covers dependency-failure isolation, chaos engineering to validate resilience, failure-mode analysis, designing to nines of availability, cost-versus-availability tradeoffs, and recovery runbooks. Spans both the patterns that isolate partial failure and the disaster-recovery planning that restores a business-critical system after a major outage.
What's the difference between graceful degradation and fail-fast behavior? Give a concrete example of when you'd want each.
Sample Answer
Direct answer
Graceful degradation keeps serving a reduced version of the response (cached data, a simplified feature set, a fallback value) when a dependency is unhealthy, trading completeness for availability. Fail-fast does the opposite: it detects the problem quickly and returns an explicit error rather than attempting a degraded response, trading availability for correctness and speed of failure signaling.
When to use each
| Graceful degradation | Fail-fast | |
|---|---|---|
| Goal | Keep the user-visible experience mostly working | Avoid doing something wrong or wasting resources |
| Good fit | Read-heavy, non-critical, or cache-friendly paths | Writes with correctness or financial consequences |
| User sees | A slightly reduced experience, often unnoticed | A clear error, immediately |
| Risk if used wrong | Serving stale or wrong data silently | Unnecessary outages for things that could have degraded fine |
| Example | Product page shows a cached price and hides personalized recommendations when the recommendation service is down | Payment endpoint rejects the request immediately when the payment gateway is unreachable, rather than guessing |
Worked example
A product detail page calls three things to render: the core product data (must succeed), a recommendations service (nice to have), and a payment-availability check (must be correct). If the recommendations service is slow or down, the page graceful-degrades by omitting that section entirely and rendering everything else; a user who never look for recommendations doesn't notice a thing, and the page stays fast because it isn't waiting on a dependency it doesn't strictly need.
If the payment gateway is unreachable when a user tries to check out, fail-fast is the right call: returning a clear "payment temporarily unavailable, please retry" immediately is far safer than attempting to guess an outcome, queue the charge silently, or degrade to some partial payment state, any of which risks a duplicate charge, a lost order, or a customer charged for something that was never fulfilled.
Trade-offs & pitfalls
The decision comes down to whether the operation is idempotent (repeating it has the same effect as doing it once, so a retry can't cause harm) and non-critical (favor graceful degradation) or has real correctness or financial stakes (favor fail-fast). The common mistake is applying one pattern uniformly across a whole service: a system that fails fast on everything, including truly optional dependencies, takes unnecessary outages; a system that gracefully degrades everything, including payment or inventory writes, risks silent data corruption that's much harder to detect and clean up after than an outage would have been.
What does idempotency mean in the context of retries, and why does it matter? Walk through how you'd make a payment-creation endpoint safe to retry, including how you'd handle the idempotency key.
Sample Answer
Direct answer
Idempotency means performing the same operation multiple times has the exact same effect as performing it once. It matters for retries because network failures make it impossible for a client to reliably tell "the request failed" apart from "the request succeeded but the response was lost"; without idempotency, a client that retries after a timeout risks creating a duplicate side effect, like charging a customer twice for one order.
Making a payment-creation endpoint safe to retry
The standard mechanism is a client-generated idempotency key attached to the request:
- The client generates a unique key (a UUID) once per logical operation, before the first attempt, and sends it on every retry of that same logical operation in a header such as
Idempotency-Key. - The server does an atomic check-and-set against a persistent store keyed by that idempotency key: if the key is new, it proceeds with the charge; if the key already exists, it returns the previously stored result instead of processing the charge again.
- The check-and-set has to be atomic (a single transactional operation, not a read followed by a separate write) so two near-simultaneous retries can't both see "key doesn't exist" and both proceed.
- The stored result includes enough to reconstruct the original response (status, charge ID, amount) and a status field (
in_progress,succeeded,failed) so a retry that arrives while the first attempt is still executing gets told to wait or gets the eventual result, rather than racing ahead. - Keys are kept with a bounded TTL (commonly 24 to 72 hours) since indefinite retention is unnecessary once a client has almost certainly given up retrying, and TTL bounds the storage cost of the idempotency table.
sequenceDiagram
participant Client
participant API as API Region A
participant Store as Idempotency Store
participant PG as Payment Gateway
Client->>API: POST charges Idempotency-Key K1
API->>Store: check-and-set K1 in_progress
Store-->>API: new key proceed
API->>PG: create charge
PG-->>API: charge succeeded
API->>Store: save result for K1
API-->>Client: 200 OK response lost in transit
Client->>API: retry POST charges Idempotency-Key K1
API->>Store: check K1
Store-->>API: found status succeeded
API-->>Client: 200 OK cached result no new charge
Worked example
In the sequence above, the server actually completes the charge and writes the success result to the idempotency store, but the client's connection drops before the 200 response arrives, so from the client's point of view the request timed out. The client retries with the same key K1. The server's check-and-set finds K1 already marked succeeded, so it returns the stored response (the original charge ID and amount) directly and never calls the payment gateway again. Exactly one charge exists, regardless of how many times the client retries.
Harder extension: retries across a cross-region failover
The same duplicate-request risk gets worse if the retry lands on a different region than the original attempt. Say the first request goes to Region A, and before the response comes back, DNS or Anycast reroutes the client (as part of a regional failover) so the retry with the same idempotency key goes to Region B. If the idempotency store is region-local and not replicated, Region B has never heard of K1, sees it as a new key, and processes a second charge, exactly the failure the mechanism was supposed to prevent.
The fix is that the idempotency store itself has to be as available and as replicated as the failover design assumes the rest of the system is: either a globally consistent store (accepting the added write latency) or, more commonly for payments specifically, delegating idempotency to the payment gateway itself, which usually supports its own idempotency keys and is already a single global system of record regardless of which region initiated the call. Relying on the gateway's own dedupe as the backstop means even a fully region-local idempotency store failing open during a failover doesn't result in a real double charge.
Trade-offs & pitfalls
Idempotency keys add a write to the hot path (the check-and-set) and a storage system that has to be highly available, since if the idempotency store itself is down, you're forced to choose between blocking the write entirely or risking a duplicate. TTL choice is a real trade-off: too short and a legitimately slow client retry after the TTL expires creates a duplicate; too long and the storage grows unnecessarily and stale in-progress records from crashed requests linger. The most common mistake is only deduplicating the write itself while forgetting downstream side effects (an email receipt, a webhook fired to a third party) that happen inside the same logical operation and need to be gated by the same check, not fired unconditionally every time the handler runs.
Compare the standard DR strategy tiers: backup-and-restore, pilot light, warm standby, and active-active multi-site. For each, what's the typical RTO/RPO range, and what does it cost you?
Sample Answer
The four standard DR tiers form a spectrum from cheapest-and-slowest to most-expensive-and-fastest, and each one trades infrastructure spend for recovery speed (RTO, recovery time objective: how long restoring service takes) and data freshness (RPO, recovery point objective: how much data, measured in time, you could lose): backup-and-restore keeps only backups running, pilot light keeps a minimal always-on core, warm standby keeps a scaled-down full copy running, and active-active multi-site keeps a full copy running and serving live traffic.
Comparing the four tiers
| Tier | What's running in DR | Typical RTO | Typical RPO | Relative cost |
|---|---|---|---|---|
| Backup-and-restore | Nothing; only backups exist in storage | Hours to a day+ (provision infra, restore data) | Hours (since the last backup) | Lowest: storage cost only |
| Pilot light | Core data store kept replicated and running; app/compute layer absent until needed | Tens of minutes to a few hours (scale up compute, deploy app) | Minutes (continuous replication to the core) | Low-moderate: one small always-on component |
| Warm standby | A scaled-down but fully functional copy of the whole stack, running continuously | Minutes (scale up capacity, redirect traffic) | Seconds to low minutes (near-real-time replication) | Moderate-high: a live, if smaller, second environment |
| Active-active multi-site | Full-scale copy in both/all sites, serving live traffic simultaneously | Near-zero (traffic reroutes, nothing to "start") | Near-zero to seconds (synchronous or tightly-bounded async replication) | Highest: full duplicate capacity plus distributed-write complexity |
The RTO/RPO ranges above are the typical shape of the trade-off, not a fixed number for any specific system: the exact figures depend on data volume, automation maturity, and how the replication is actually implemented within each tier.
Worked example: a budget-constrained startup
A mid-sized SaaS with a fixed infrastructure budget doesn't have to pick one tier for the whole system; the standard move is to mix tiers by criticality. Say the product has three logical components: authentication/billing (must never meaningfully go down, since it blocks every paying customer from doing anything), the core application (needs to come back reasonably fast but a short outage is tolerable), and internal admin tooling (only the ops team notices if it's down for a few hours).
A budget-conscious allocation: active-active for auth/billing (the one component where the cost premium is justified because its outage blocks revenue entirely, and it's usually small enough in infrastructure footprint that duplicating it fully is affordable), pilot light for the core application (keep the database replicated continuously so RPO stays low, but only spin up the app-server fleet in DR when actually needed, since that's the majority of the compute cost), and backup-and-restore for admin tooling (cheapest tier, acceptable because nobody customer-facing is blocked by it being down for hours). This gets the highest-blast-radius component the fastest recovery while keeping the overall DR bill proportional to what each component actually costs the business if it's down, instead of buying active-active everywhere by default.
Trade-offs and pitfalls
The most expensive mistake in this space isn't picking the "wrong" tier, it's picking a tier and never testing failover into it: a pilot-light setup that's never actually been promoted to full capacity under load is a theoretical RTO, not a real one, and the first real DR event is a bad time to discover the app layer doesn't actually scale up cleanly from zero. A related pitfall is under-provisioning a warm standby's capacity: "scaled down" often means it can absorb DR traffic at reduced performance, and teams sometimes forget to validate that the scaled-down size can actually handle 100% of production load once promoted, not just serve health checks. Finally, active-active's real cost isn't just the duplicate infrastructure line item, it's the ongoing engineering cost of keeping a multi-writer data model correct, which is easy to underestimate when comparing tiers purely on an RTO/RPO/dollar table.
A downstream service you depend on starts responding slowly, and requests to it start backing up on your side, growing queues and increasing latency. Walk through your immediate mitigations and your longer-term architectural fix, and explain the trade-off each one introduces.
Sample Answer
Direct answer: The immediate priority is to stop the slowdown from consuming your own resources: set aggressive timeouts, open a circuit breaker so you stop calling the failing dependency, and isolate the connection/thread pool used for that call so it can't starve everything else. The longer-term fix is architectural: decouple the caller from the dependency's latency entirely, usually via an async queue or by making the call non-blocking, so a slow downstream degrades throughput instead of taking the whole service down with it.
Structured elaboration
Why this happens (the mechanism): by Little's Law, the number of requests in flight L equals arrival rate λ times the time each request spends in the system W: L=λW. If a downstream call's latency goes from 50ms to 500ms while your request rate stays at, say, 200 requests/second, the in-flight count grows from L=200×0.05=10 to L=200×0.5=100, a 10x increase, purely from the latency change with no change in incoming traffic. If your thread or connection pool was sized for ~10-20 concurrent in-flight requests to that dependency, it's now exhausted, and requests start queueing on your side, which is exactly the symptom described.
Immediate mitigations (minutes, not a redesign):
| Mitigation | What it does | Trade-off it introduces |
|---|---|---|
| Tight timeouts | Caps how long you'll wait, preventing unbounded queue growth | Cuts off requests that might have succeeded a moment later; needs to be shorter than your own SLA to the caller |
| Circuit breaker | Stops calling the dependency once error/latency crosses a threshold, failing fast instead of queueing | Can trip on transient blips if thresholds are too sensitive; denies service even to calls that might succeed |
| Bulkhead (isolated pool) | Gives this dependency its own thread/connection pool so its slowdown can't exhaust pools shared by healthy dependencies | Reduces pooled efficiency (can't borrow capacity across dependencies); requires knowing sizing up front |
| Load shedding / fast 503 | Rejects excess requests immediately when queue depth crosses a threshold, protecting the instances still healthy | Directly reduces availability for shed requests; needs to shed selectively, not randomly, if some requests matter more |
Longer-term architectural fix:
- Decouple via an async queue: put a durable queue between the caller and the slow dependency so the caller can return quickly (accept-and-acknowledge) and the dependency is drained at its own sustainable pace, rather than the caller blocking on it synchronously. Trade-off: the caller can no longer return a synchronous success/failure for that operation; the interaction model has to change to something the client and product can tolerate (a "pending" state, a webhook, a poll).
- Idempotent retries with backoff and jitter: if retries are needed, they must be capped, exponential, and jittered so a fleet of callers doesn't retry in lockstep and re-create the exact overload it's recovering from. Trade-off: added complexity, and retries must be provably idempotent on the downstream side or they risk duplicate side effects.
- Capacity planning against the tail, not the average: provision the dependency (or the pool sized to call it) based on observed p99 latency, not p50, since it's the tail that determines when queues start building. Trade-off: costs more standing capacity for headroom that's idle most of the time.
Applying this to concrete variants of the same pattern: the reasoning above is the same whether the slow dependency is a payment-validation service (immediate: circuit breaker + fast-fail with a clear "try again" to the user rather than a silent hang; long-term: async payment confirmation via webhook), a message-queue consumer falling behind (immediate: shed or dead-letter the oldest low-priority messages, bulkhead the consumer pool by message type; long-term: scale consumers horizontally and partition by priority), a retry storm from a flood of client-side 503s (immediate: the client-side backoff-with-jitter above is the direct fix; long-term: make the shedding threshold adaptive so it doesn't itself become the trigger for a thundering herd), or a synchronous order-processing pipeline backing up (immediate: bulkhead the slow stage's pool; long-term: convert that stage to the async-queue pattern above).
Trade-offs & pitfalls
- Every immediate mitigation above trades some availability or correctness for stability: timeouts drop requests that might have succeeded, circuit breakers deny service during their open window, load shedding sacrifices some requests to save the rest. The point isn't to avoid the trade-off, it's to make it deliberately and visibly rather than let an unbounded queue make it for you via an eventual crash.
- A common wrong turn: adding retries as the first response to a slowdown. Naive retries without backoff amplify load on an already-struggling dependency and can turn a partial slowdown into a full outage (a retry storm).
- Circuit breakers and bulkheads need to be tuned against real traffic and latency distributions; thresholds copied from a different service's runbook are a common source of either false trips (unnecessary unavailability) or no protection at all (thresholds too loose to matter).
What does graceful degradation mean for a resilient system, and why does it matter? Pick a user-facing service, like search or checkout, and walk through which features you'd disable first under partial failure, and which you'd protect at all costs.
Sample Answer
Direct answer: Graceful degradation means a system keeps serving its core value under partial failure by deliberately shedding non-essential features, instead of failing completely because one dependency is unhealthy. It matters because most real outages are partial, not total, and a system that can't distinguish "checkout is down" from "product recommendations are down" ends up treating both the same way: total outage, when only one of them actually deserved it.
Structured elaboration
The core discipline is ranking features by how essential they are to the user's actual goal, then deciding in advance what happens to each tier when its supporting dependency fails:
| Priority | Category | What happens under partial failure |
|---|---|---|
| Protect at all costs | The core transaction (e.g., add to cart, checkout, payment) | Never disabled; if its own dependency fails, fail the request loudly rather than silently corrupt it |
| Degrade first | Personalization and enrichment (recommendations, "customers also bought," rich previews) | Hide the widget or fall back to a generic/cached version; the page still loads and functions |
| Degrade next | Non-critical background work (analytics events, telemetry sampling, async inventory sync) | Drop or buffer, since losing this doesn't affect the current user's experience |
How you decide what's "core": ask whether the feature is on the path the user came for. For a checkout service, that's the cart-to-payment path; product recommendations, reviews, and "recently viewed" are enrichment around that path, valuable but not why the user is there. For a search service, returning some relevant results is core; typo-correction, personalized re-ranking, and query autocomplete are enrichment that can be dropped without breaking the user's ability to search.
Detecting when to degrade: this has to be automatic, not something a human decides mid-incident. Health checks and latency/error-rate thresholds on each dependency feed a circuit breaker; when the breaker for the recommendations service opens, the front end (or an API gateway) simply omits that section rather than waiting on a call that's failing. The degraded state should be visible in monitoring (a "degraded mode" flag, not silence) so the team knows it's active and can address root cause.
Trade-offs & pitfalls
- Degrading too aggressively removes revenue-generating features (recommendations often drive real conversion) for failures that didn't actually require it; the tiering has to be based on actual dependency health, not a blanket "anything non-core gets cut."
- Degrading too conservatively (waiting too long, or requiring a human to flip a switch) means the cascading failure the degradation was supposed to prevent happens anyway, because by the time a human reacts, the core path is already backed up.
- Static thresholds don't generalize across traffic levels; a latency threshold tuned for average traffic can either never trigger during a real incident at peak load, or trigger too eagerly during a routine traffic spike that isn't actually a failure.
- Testing degraded paths is easy to skip because they're rarely exercised in normal operation; without deliberately forcing dependencies to fail in staging (or via chaos testing in production), the first real test of the degraded path is during an actual incident, which is the worst time to discover it's broken.
- The same tiering logic applies outside typical web services: an ML-serving system facing a slow or unavailable model can fall back to a cached prior response, swap to a smaller/cheaper model that's faster but less accurate, or return a safe default decision, the exact same "protect the core interaction, shed the enrichment" reasoning, just with "model quality" instead of "page richness" as the thing being traded off.
Unlock Full Question Bank
Get access to all Fault Tolerance, High Availability, and Disaster Recovery interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.