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.
Design the failure detection that decides when to trigger an automated failover for a critical service. What health signals would you check, how would you set thresholds and windows to avoid mistaking a blip for a real failure, and when would you still want a human in the loop instead of a fully automatic failover?
Sample Answer
Direct answer: I'd layer health signals from cheap-and-fast (process liveness) to expensive-and-meaningful (dependency and business-metric checks), require multiple consecutive failures before declaring a node unhealthy to avoid reacting to a single blip, and keep a human in the loop specifically for the step that's hardest to reverse: promoting a new primary for stateful services, where an incorrect automated failover can cause data loss or a split-brain (two nodes each believing they're the one true primary and accepting conflicting writes at the same time), versus something like removing an unhealthy instance from a load-balancer pool, which is cheap to reverse and safe to fully automate.
Structured elaboration
| Signal | What it catches | Suggested check interval | Failures needed before acting |
|---|---|---|---|
| Process liveness | Process crashed or hung | Every 5s | 3 consecutive (15s) |
| Readiness / dependency connectivity | Process is up but can't reach its DB, cache, or queue | Every 10s | 2 consecutive (20s) |
| Latency / error-rate threshold | Process is up and connected, but degraded (slow, erroring) | Every 10-15s, rolling window | Sustained breach over a window (e.g., p95 > threshold for 3 samples), not a single sample |
| Business-metric sanity | Everything upstream looks healthy but the service is doing something wrong (e.g., checkout success rate collapsed) | Every 30-60s | Requires a real threshold breach, not a spike; slower-moving signal used as a final gate |
Why "N consecutive failures" instead of one: a single failed check can be a genuine blip (a GC pause, a brief network hiccup) rather than a real failure. If each check independently has some baseline flakiness probability p (a transient failure unrelated to a real outage), then requiring N consecutive failures before declaring unhealthy makes the false-trigger probability:
P(false trigger)=pNWith, say, p=0.05 (5% chance any single check fails transiently) and N=1 (react on the first failure), the false-trigger probability is just p=5%, meaning roughly 1 in 20 blips would incorrectly trigger action. Requiring N=3 consecutive failures drops that to:
P(false trigger)=0.053=0.000125=0.0125%a 400x reduction, at the cost of a real failure now taking 3 check intervals (here, up to 15 seconds at a 5s interval) longer to detect instead of one. That's the actual dial being turned: detection speed versus false-positive rate, and it should be set from an observed flakiness rate for your specific checks, not copied from another team's runbook.
Decision flow, including where automation stops and a human is required:
flowchart TD
A[Health check runs] --> B{N consecutive<br/>failures?}
B -->|No| A
B -->|Yes| C{What kind of<br/>action?}
C -->|Remove from LB pool| D[Fully automated:<br/>cheap, instantly reversible]
C -->|Open circuit breaker| D
C -->|Promote new primary<br/>for stateful service| E{Safety checks pass?<br/>replica caught up,<br/>quorum reachable}
E -->|No| F[Page human,<br/>do not auto-promote]
E -->|Yes, and blast radius<br/>is well-understood| G[Auto-promote,<br/>but page for review]
E -->|Yes, but ambiguous<br/>e.g. partition, not clear failure| F
"Quorum reachable" in that safety check means enough replicas are online and able to vote that a new primary can be safely elected, a majority agreeing on who's in charge, without risking two nodes each believing they're the primary at once.
Trade-offs & pitfalls
- Fully automated failover is right when the action is cheap and reversible (removing an unhealthy node from rotation); it's risky when the action is expensive or irreversible (promoting a database replica, since promoting the wrong one, or promoting during a network partition rather than an actual failure, can cause split-brain or data loss). The line isn't "how critical is the service," it's "how reversible is this specific action."
- Requiring consecutive failures trades detection speed for false-positive protection; too aggressive a requirement (e.g., N=10) means a genuine failure runs uncaught far longer than the blast radius justifies.
- Checks that themselves depend on a shared resource (e.g., every health check queries the same central database) can produce correlated, simultaneous "failures" across an entire fleet when that shared resource degrades, which looks like a mass outage but is really a single point of failure in the monitoring path itself.
- A common wrong turn: only checking process liveness and assuming that's sufficient. A process can be alive, passing liveness checks, and still be completely unable to serve real traffic because its only database connection pool is exhausted; readiness and dependency checks catch what liveness checks structurally cannot.
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.
What is chaos engineering, and why would a company deliberately break its own production systems on purpose? Walk through the basic methodology: how you'd define steady state, form a hypothesis, and run a safe first experiment.
Sample Answer
Chaos engineering is the practice of deliberately injecting failure into a system, in a controlled way, to find weaknesses before they find you during a real incident. The reasoning behind doing it on purpose: most production failures aren't hypothetical, dependencies do time out, nodes do crash, networks do partition, and the choice isn't between "failures happen" and "failures don't happen," it's between discovering how your system responds to them during a planned, low-stakes experiment or during an unplanned, high-stakes 3 a.m. page.
Methodology
1. Define steady state. Pick measurable indicators of normal health, request success rate, latency percentiles, throughput, that represent "the system is working" in terms an on-call engineer would actually check on a dashboard, not an abstract notion of "healthy."
2. Form a hypothesis. State, before running anything, what you expect to happen and why: "if we kill one instance of the recommendation service, overall page error rate will stay flat because the client has a fallback path." A real hypothesis is falsifiable; "let's see what happens" isn't chaos engineering, it's just causing an outage without a way to learn from it.
3. Design a safe first experiment. Choose the smallest fault that could test the hypothesis (kill one non-critical replica, not the whole fleet) and decide the blast radius up front: what fraction of traffic or users can be affected, and for how long.
4. Run it with an abort condition already defined. Before starting, decide the exact metric threshold that ends the experiment immediately (for example, page error rate exceeding a set ceiling), so the decision to stop isn't made under pressure in the moment.
5. Observe against the steady-state baseline. Watch the same metrics defined in step 1, not new ones invented mid-experiment, so the comparison is apples-to-apples.
6. Learn and iterate. If the hypothesis held, expand the blast radius gradually on future runs. If it didn't, that's the actual finding, fix the missing fallback or retry logic, and re-run the same experiment to confirm the fix works before calling it done.
Worked example: a first, safe experiment
Target: a non-critical "related items" widget on a product page, deliberately chosen because a broken hypothesis here degrades a widget, not checkout. Steady state: page load success rate and p95 latency, whatever their current normal values are for that page. Hypothesis: "terminating one replica of the related-items service will not change page load success rate or p95 latency, because the front end treats that service as optional with a client-side timeout and empty-state fallback." Experiment: kill one replica (not all of them) during a low-traffic window, with an abort condition of "page success rate drops below its normal range" defined before starting. Outcome either confirms the fallback works as designed, or reveals it doesn't, which is the actual value of running it: finding that out on a Tuesday afternoon experiment instead of during a real node failure at peak traffic.
Trade-offs and pitfalls
The most common misunderstanding is that chaos engineering means "randomly break things in production," when the entire method is built around the opposite instinct: a stated hypothesis, a bounded blast radius, and a predefined abort condition are what separate a chaos experiment from just causing an outage. A related pitfall is skipping the hypothesis step and injecting a fault "to see what happens": without a stated expectation, there's no way to say afterward whether the result was surprising or how bad it was relative to what should have happened. Teams also sometimes skip straight to production chaos before validating the tooling and abort mechanism in staging first, running the injection and rollback machinery against a stage environment is itself a smaller, safer experiment worth doing before trusting it against real traffic.
What's the difference between N+1 and N+2 redundancy? For a service normally sized at 10 instances, walk through what each strategy actually buys you in failure tolerance, and when the extra cost of N+2 is worth it.
Sample Answer
Direct answer: N+1 means you provision one spare unit beyond what's needed to serve current load, so the system tolerates exactly one simultaneous failure with zero capacity loss. N+2 provisions two spares, tolerating two simultaneous failures (or one failure plus a second one arriving while the first is still being repaired). For a service sized at 10 instances, N+1 is 11 instances and N+2 is 12; the extra instance in N+2 is worth it when failures are likely to be correlated or when repair (MTTR) is slow enough that a second failure landing during the first one's recovery window is a real possibility, not a hypothetical.
Structured elaboration
- What "N" means: N is the number of units actually required to serve load at your target performance, not the number you happen to run. If 10 instances are needed to handle peak traffic at acceptable latency, N=10.
- N+1: one extra unit. Any single instance, host, rack, or power supply can fail and the system still serves at full capacity from the remaining N. It does not protect against a second, overlapping failure.
- N+2: two extra units. Protects against two simultaneous failures, which matters specifically during the repair window of the first failure (you're running on N+1 capacity while node 1 is being replaced; if node 2 fails during that window, N+1 would drop you below N, but N+2 still covers you).
- When N+2's extra cost is worth it: the decision comes down to how correlated failures are and how long repair takes, not just how critical the service is in the abstract.
Worked example: quantifying the risk N+2 removes
A capacity shortfall only happens when multiple instances are down at the same time, which means the model has to use the instantaneous probability that an instance is down at any given moment, not the probability that it fails at some point during the year (an annual failure probability answers a different question and silently ignores repair-window overlap). The right building block is the instantaneous-unavailability formula: at any random moment, the fraction of time a single instance has historically spent broken and being repaired is MTTR divided by the full working-plus-repair cycle, MTBF+MTTR, which is exactly the probability that instance happens to be down at an arbitrary moment in time:
q=MTBF+MTTRMTTRPin illustrative values: each instance has an MTBF of 8,760 hours (fails on average about once a year) and an MTTR of 4 hours (time to detect and replace or restart a failed instance). Then:
q=8760+44=87644≈0.000456(0.0456%)That's the probability any single instance is down (mid-repair) at a random moment.
For N+1 (11 total instances), capacity drops below the needed N=10 only if 2 or more instances are down simultaneously:
P(down≥2∣n=11,q)=1−(011)(1−q)11−(111)q(1−q)10 =1−0.994991−0.004998=0.0000114(0.00114%)For N+2 (12 total instances), capacity drops below 10 only if 3 or more are down simultaneously:
P(down≥3∣n=12,q)=1−k=0∑2(k12)qk(1−q)12−k =1−0.994537−0.005450−0.0000137=0.0000000209(0.0000021%)Both numbers are tiny snapshot probabilities; the more useful reading is as the expected fraction of the year the system spends in a shortfall state, converted into expected annual downtime minutes by taking that same fraction-of-time-in-shortfall and multiplying it by the number of minutes in a year, 525,600 (365 days x 24 hours x 60 minutes), the standard way a fraction-of-time becomes an annual downtime figure:
N+1: 0.0000114×525,600≈6.0 minutes/year N+2: 0.0000000209×525,600≈0.011 minutes/year(≈0.66 seconds/year)So under this repair-window-conditioned model, N+1 carries about 6 minutes/year of expected capacity-shortfall exposure, and N+2 cuts that to about 0.01 minutes/year, roughly a 548x reduction, not because any instance's individual failure rate changed, but because a shortfall now requires a second failure to land inside the narrow repair window of the first, and adding a spare pushes that bar from "2 simultaneous" to "3 simultaneous," a much rarer event once q is small. Whether that ~6-minute-a-year difference is worth one extra instance's cost is exactly the trade-off to walk through out loud: for a service where even a few minutes of capacity shortfall risks an SLA breach, cutting expected exposure by roughly two and a half orders of magnitude for one extra instance is usually cheap insurance; for an internal batch service, shortfall risk this small to begin with is very likely not worth the extra spend.
Common concrete instances of the same reasoning: UPS/power-supply sizing (N+1 power modules in a rack survive one PSU failure; N+2 covers one failed unit plus one more failing during the swap), and network device sizing (N+1 top-of-rack switches vs N+2 when switch firmware upgrades take units offline for extended maintenance windows, effectively acting like a "planned failure" that N+1 alone can't absorb if an unplanned one happens at the same time).
Trade-offs & pitfalls
- N+2 isn't "more reliable" in a vacuum, it's specifically insurance against overlapping failures; if your MTTR is minutes and failures are rare and independent, N+1 is usually sufficient and N+2 is paying for a scenario that almost never occurs.
- Fault-domain correlation matters more than the raw redundancy count: N+1 spread across a single rack doesn't protect against a rack-level power failure taking out several "independent" instances at once; redundancy has to be placed in genuinely independent failure domains (different racks, AZs, or power feeds) or the N+1/N+2 math above doesn't hold, because the independence assumption breaks.
- A common wrong turn: treating N+1 as "one extra instance total" when instances are correlated (e.g., all on the same physical host or the same AZ). The formula only protects capacity if the spare's failure mode is independent of the others.
- N+2 costs roughly 20% more standing capacity than N+1's 10% here; that recurring cost has to be justified against the downtime cost it avoids, not assumed.
What is the circuit breaker pattern? Walk through its states, closed, open, and half-open, what triggers each transition, and how you'd choose the failure threshold and time window for a real dependency.
Sample Answer
The circuit breaker pattern stops calling a failing dependency once it's clearly unhealthy, so callers fail fast instead of piling up waiting on a dependency that isn't going to answer, and the dependency gets breathing room to recover instead of being hit with an ever-growing retry storm on top of whatever's already wrong with it.
The three states
stateDiagram-v2
[*] --> Closed
Closed --> Open: failure threshold crossed
Open --> HalfOpen: cooldown elapses
HalfOpen --> Closed: probe requests succeed
HalfOpen --> Open: probe requests fail
| State | Behavior | What triggers the next transition |
|---|---|---|
| Closed | Calls pass through normally | Error rate or consecutive failures cross a defined threshold within the tracking window |
| Open | Calls fail immediately (or return a fallback); the dependency isn't called at all | A fixed cooldown period elapses |
| Half-open | A small number of probe requests are allowed through to test recovery | Probes succeed (close the breaker) or fail (reopen it, usually with a longer cooldown) |
Choosing the threshold and window for a real dependency
Base the threshold on the dependency's own historical baseline, not a round number picked by feel: if a dependency's normal error rate is 1-2%, a threshold like "error rate exceeds 50% over a 1-minute window" is a real signal of degradation, not noise. Combine multiple signals rather than trusting one: an error-rate threshold alone can be fooled by a burst of retriable timeouts, so pairing it with a consecutive-failure count and a latency percentile (for example, p99 exceeding a set ceiling) catches degradation that shows up as slowness before it shows up as outright errors.
Worked example: why the half-open probe count matters
Say the breaker opens, waits out its cooldown, and moves to half-open, sending 5 probe requests before deciding whether to close. If the dependency is still genuinely degraded, with a true underlying failure rate of p=0.3 (30% of calls failing), the probability that all 5 probes happen to succeed by chance despite that is:
P(all 5 probes succeed)=(1−p)5=(0.7)5≈0.168(16.8%)That's not a rare fluke, it's roughly a 1-in-6 chance of prematurely closing the breaker on a dependency that's still 30% broken, which then immediately re-floods it with full traffic and likely reopens the breaker on the very next window. This is the concrete argument for either using more probes (the same calculation with 10 probes drops the false-close probability to 0.710≈0.028, about 2.8%) or ramping traffic gradually after a half-open success instead of jumping straight from 5 probes to 100% traffic.
Trade-offs and pitfalls
Setting the threshold too sensitive (a low error-rate bar or a short window) causes flapping: the breaker opens on transient noise, degrades the user experience with unnecessary fallbacks, and can itself become a source of alerts nobody trusts. Setting it too lax delays protection long enough for the caller's own retries and connection-pool exhaustion to cascade into a second incident on top of the first. The half-open probe-count math above is the same trade-off in miniature: too few probes risk a premature, false-positive close; too many probes delay recovery and keep failing extra requests during the test window. In practice this is tuned with production data and game-day testing rather than picked once and left alone, and the same three-state logic applies regardless of what's on the other side of the call, an AI inference endpoint that starts throwing GPU-OOM errors under load trips the same breaker, on the same threshold logic, as a slow downstream REST dependency; only the specific error signal being watched changes.
Unlock Full Question Bank
Get access to all 16 Fault Tolerance, High Availability, and Disaster Recovery interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.