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 does 'blast radius' mean when you're talking about a production failure? Name a few concrete engineering practices that reduce it, and what that costs you.
Sample Answer
Direct answer
Blast radius is the scope of impact when a component fails: how many users, tenants, or dependent services are affected, and how severely, not just whether the failure happened at all. Reducing blast radius means designing so a single failure touches the smallest possible slice of the system, which makes outages smaller, easier to detect, and faster to recover from, even if it doesn't reduce how often failures happen at all.
Practices that reduce it, and what they cost
| Practice | How it shrinks blast radius | What it costs |
|---|---|---|
| Circuit breakers | Stop repeated calls to a failing dependency, isolating the failure to the caller instead of letting it spread | Added latency and complexity in the failure path; a poorly tuned breaker can trip on transient blips |
| Finer-grained service decomposition | A failure or overload in one bounded service only affects its own consumers, not unrelated functionality | More services to deploy, monitor, and operate; cross-service calls add their own new failure modes |
| Bulkheads (per-tenant or per-dependency resource pools) | One tenant's or one dependency's exhaustion doesn't consume capacity meant for everyone else | More total resources provisioned (dedicated pools cost more than one shared pool sized for the average case) |
| Traffic shaping and rate limits | Caps how much load a single misbehaving client or spike can push into downstream systems | Legitimate bursty clients can get throttled unless limits are tuned carefully |
Worked example
Consider a service with 1,000 tenants sharing a single connection pool. If that pool exhausts, every tenant is affected. Now split that same total capacity into 10 isolated pools of 100 tenants each, so each pool serves 100 of the 1,000 tenants and only that pool's own tenants are affected if it exhausts:
1,000100=10% of tenants affected (isolated pools)vs.100% (shared pool)Splitting the same total capacity into 10 pools of 100 tenants each means a single pool's exhaustion now affects only 100 of the 1,000 tenants, 10% of the blast radius of the shared-pool design, for the same total resources. The cost is operational: 10 pools to monitor and size instead of one, and if traffic isn't evenly distributed across tenants, some pools may be under-utilized while others are tight, which the shared pool didn't have to worry about.
Trade-offs & pitfalls
Reducing blast radius is generally a trade of operational complexity and some resource inefficiency for smaller, more contained failures; it doesn't reduce the underlying failure rate of any individual component. The common mistake is treating blast-radius reduction as free: partitioning by tenant, region, or dependency multiplies the number of things to monitor and can hide a systemic bug (one that affects every partition equally) behind what looks like ten separate, unrelated small incidents instead of one clearly systemic one.
Design an automated system that regularly verifies your backups are actually restorable, not just that the backup job succeeded. What would you check, how would you measure it against your RTO, and how would you alert when verification fails?
Sample Answer
Direct answer
Verifying backups means periodically doing a real restore into an isolated environment, checking the data is intact and the application actually works on it, and measuring how long that took against your RTO. A backup job exiting with status 0 only proves bytes were written somewhere; it says nothing about whether those bytes are usable or how fast you could get back up.
Architecture
flowchart LR
A[Scheduler] --> B[Fetch Latest Backup Snapshot]
B --> C[Isolated Restore Environment]
C --> D[Data Integrity Checks]
D --> E[App-Level Smoke Test]
E --> F[Measure Restore Duration]
F --> G{Duration vs RTO Budget}
G -->|Within budget| H[Record Pass and Metrics]
G -->|Exceeds 80% of RTO| I[Warning Alert]
G -->|Exceeds RTO or integrity fail| J[Page On-Call]
C --> K[Auto-Teardown Environment]
What gets checked, in layers:
- Object-level integrity: checksum or hash comparison against the value recorded at backup time, so silent bit-rot or a truncated upload is caught before restore even starts.
- Structural integrity: for a database, native consistency checks, row counts against expected ranges, and foreign-key integrity after load.
- Application-level correctness: boot the restored data behind a real instance of the service and run a small suite of read/write smoke transactions. This is the layer that catches "the schema loaded fine but the app can't actually serve a request," which checksums alone never will.
Isolation requirements: the restore environment is network-isolated from production (no shared VPC routes), uses least-privilege IAM scoped only to that environment, and is torn down automatically after each run so it doesn't become a second, unmonitored copy of sensitive data sitting around.
Cadence: critical systems get a restore of a representative sample daily and a full restore weekly; lower-tier systems get weekly sampled restores and a monthly full restore. Rotating which shard or tenant gets sampled means every partition gets exercised over a few weeks without paying for a full restore every night.
Worked example
Take a Postgres cluster with an RTO of 4 hours (240 minutes). A weekly synthetic restore test times each stage:
155=45+90+20 minutes measured restore time(45 min to pull and attach the snapshot, 90 min to load schema and data, 20 min for integrity checks and smoke tests.)
Compared against the RTO budget:
85=240−155 minutes of RTO headroomThat headroom is not static. If data volume growth is pushing restore time up by roughly 15 minutes per week (visible by trending the weekly measurement), you can compute how much runway is left before the RTO is silently violated:
1585≈5.67 weeks until RTO breach at this growth rateThat is the number that should drive a proactive change (parallelizing the load step, moving to physical replication instead of logical restore, or revisiting the RTO itself) before an actual incident forces it. For alerting thresholds, an early warning fires well before the hard breach:
192=240×0.8 minutes, the early-warning thresholdA hard page fires immediately on either an integrity-check failure or a measured restore time over 240 minutes; the 192-minute warning gives the team a chance to act before the RTO itself is at risk.
Trade-offs & pitfalls
Full restores give the strongest confidence but cost real compute and time, so most teams sample a representative subset for frequent runs and reserve full restores for a weekly or monthly cadence. Masking or redacting PII in the restored copy is often a compliance requirement, but it adds time and complexity to the pipeline, so it needs its own budget inside the RTO measurement rather than being treated as free.
The most common mistake is treating "restore job succeeded" as the finish line. A restore that completes but never boots the application, or one measured on a laptop-sized test dataset instead of a representative sample, produces a false sense of safety. The other frequent gap is forgetting to track the restore-time trend over time; a system that passes today but is quietly getting slower every week will fail its RTO exactly when it matters most, with no warning if only pass/fail is alerted on rather than the duration trend itself.
How would you use feature flags to enable graceful degradation under partial failure? Walk through an example where you'd turn off a non-essential feature to protect the core experience, and how that differs from using a flag purely as an incident-response kill switch.
Sample Answer
Direct answer
A feature flag enables graceful degradation by gating a non-essential feature so it can be turned off (either automatically, when its dependency is unhealthy, or manually, by an operator) without a deploy, falling back to a safe default so the core experience keeps working. That's a different use of a flag than a pure incident-response kill switch: a degradation flag is designed in from the start as part of the feature's normal operation, while a kill switch is an emergency-only override bolted on for when something unrelated goes wrong.
Design and example
Consider a personalized recommendations feature on an ecommerce product page, which is genuinely optional; the core browsing and checkout flow must keep working regardless of its state.
| Degradation flag (planned) | Kill switch (incident-only) | |
|---|---|---|
| Trigger | Automatic: circuit breaker trips, or the flag is tied to the dependency's health check | Manual: an on-call engineer flips it during an incident |
| Designed for | Normal, expected partial failure | Unplanned, severe failure requiring immediate mitigation |
| Fallback behavior | Pre-built, tested fallback (cached or rule-based recommendations) | Often just "off," with no fallback content designed |
| Rollout | Progressively tested (canary percentages) before trusting it in production | Rarely exercised until the incident that needs it |
Wiring it up:
- The recommendations call sits behind a timeout (roughly 300ms) and a circuit breaker that opens after a run of failures.
- The flag itself is a server-side toggle, checked before the call is made, with a traffic-percentage dial for progressive rollout: 0% then 1% (canary) then 10%, 50%, 100%, watching error rate and latency at each step.
- When the flag is off, or the circuit breaker is open, the page serves a pre-built fallback (for example, top-selling items for the category) rather than an empty section, so the degradation is graceful rather than visibly broken.
- A separate, always-available global override lets an on-call engineer force the flag off immediately during an incident, independent of the automatic circuit-breaker logic; this is the kill-switch behavior layered on top of the same flag.
Worked example
During rollout, the recommendations flag is raised from 1% to 10% of traffic. At the 10% stage, latency on the recommendations call spikes and the circuit breaker trips automatically for the affected traffic slice; those users immediately see the rule-based fallback instead of an error or a hung request, while the other 90% of traffic (flag still off) is entirely unaffected. Because the fallback content was built and tested as part of the flag's design, this looks like an intentional, contained event on the dashboards, a spike in fallback-rate, not an outage. If the same latency spike had occurred with no flag at all, the failure would have shown up as a broad, undifferentiated error-rate increase with no automatic containment.
Trade-offs & pitfalls
Every degradation flag adds a second code path (the fallback) that needs its own testing and needs to stay correct as the primary feature evolves; a fallback nobody has exercised in months is a latent bug waiting for the one day it's actually needed. Flag proliferation is the other real cost: flags that outlive their purpose (a kill switch added for a resolved incident and never removed) accumulate as technical debt and make the system's actual behavior harder to reason about. The fix is treating flag removal as part of the incident's follow-up work, not an optional cleanup task, and periodically auditing which flags are still load-bearing versus dead weight.
Not every service in your portfolio needs the same level of redundancy. Walk through how you'd tier services (say critical, important, noncritical) and what redundancy level and SLO target you'd assign to each tier, and why.
Sample Answer
Direct answer: Not every service deserves the same redundancy budget, because redundancy costs money and operational complexity that only pays off if the service's failure actually hurts the business. I'd tier services by blast radius (does it touch revenue, does it touch customer trust, is there a regulatory SLA) rather than by how technically interesting the service is, and assign each tier a concrete SLO (service level objective: the target reliability number the team commits to hitting, e.g. 99.9% uptime), redundancy model, and error budget derived from that SLO, not an arbitrary one.
Structured elaboration: a three-tier model
| Tier | Example services | SLO target | Redundancy model | Monthly error budget |
|---|---|---|---|---|
| Critical | Checkout, auth, payment processing | 99.99% | Active-active across 2+ regions, automated failover | (1−0.9999)×730×60=4.38 min |
| Important | Search, recommendations, internal APIs other teams depend on | 99.9% | Active-passive cross-region or multi-AZ single region, automated failover | (1−0.999)×730×60=43.8 min |
| Noncritical | Internal admin tools, batch analytics, nightly reports | 99.0% | Single-region, autoscaled, manual recovery acceptable | (1−0.99)×730×60=438 min (≈ 7h 18m) |
The error budget column isn't asserted, it's derived from the SLO the same way allowed downtime is derived from an availability target: budgetmonth=(1−A)×730×60 minutes. Notice the tiers are roughly 10x apart from each other in budget (4.38 to 43.8 to 438 minutes), which mirrors the 10x-per-nine relationship and makes the tiers easy to reason about and communicate.
How I'd assign a service to a tier:
- Revenue/legal impact: does an outage stop money from moving, or breach a contractual SLA? If yes, floor is Critical regardless of traffic volume.
- Blast radius to other teams: is this a shared dependency other services call synchronously? A "low traffic" internal auth service can still deserve Critical tier because everything downstream inherits its outage.
- User-facing vs. internal: internal tools default one tier lower than an equivalent customer-facing service, unless (2) applies.
- Recoverability: if a noncritical batch job fails, does it self-heal on the next run with no lasting damage? If yes, it can sit at 99% even if "important" sounding, because the cost of a missed run is low.
Worked example applying the model: a checkout service (Critical, 99.99%, active-active) calls a recommendations service (Important, 99.9%) for a "customers also bought" widget. If recommendations goes down, checkout's tier assignment only holds if checkout is built to degrade gracefully when that dependency fails (skip the widget, keep processing the order), rather than blocking checkout on it. This is the practical reason tiering isn't just about the service itself, it forces you to also classify every dependency edge as "must be up for me to meet my SLO" or "nice to have," which is often the more useful output of the exercise than the tier label itself.
Trade-offs & pitfalls
- Over-tiering (calling too many services Critical) defeats the purpose: if everything is active-active multi-region, you've spent the budget without actually differentiating risk, and you've made the org slower to ship everywhere instead of faster where it doesn't matter.
- Tiers need a review cadence. A service that started as an internal tool and quietly became load-bearing for a customer-facing flow needs to be re-tiered; without a periodic review, tier assignments rot and stop reflecting actual blast radius.
- Political pressure to over-tier is common (every team believes their service is critical); the fix is grounding tier assignment in the revenue/legal/blast-radius questions above with a named approver, not a self-declared label.
- A common wrong turn: tiering the service but not its dependencies. A Critical-tier service that synchronously calls a Noncritical-tier dependency on its hot path has effectively downgraded itself to the dependency's tier, no matter what its own SLO document says.
Define cascading failure and walk through a realistic example: service C fails, B (which depends on C) gets overloaded, and A (which depends on B) starts degrading too. At each layer, what protection would you put in place to stop the cascade from propagating?
Sample Answer
Direct answer
A cascading failure is when one component's failure increases load or latency on the components that depend on it, and that increased load causes those components to fail too, propagating outward until a large part of the system is affected, even though only one component actually broke in the first place. The mechanism is almost always resource exhaustion: threads, connections, or memory tied up waiting on the failed component instead of being freed quickly.
Walkthrough: C fails, B overloads, A degrades
flowchart LR
A[API Gateway] -->|rate limit and timeout| B[Order Service]
B -->|bulkhead pool: payments| C[Payment Service]
C -.fails.-> B
B -->|circuit breaker opens| D[Fallback: queue order for async retry]
A -->|circuit breaker opens| E[Fallback: 503 with Retry-After]
B -->|isolated pool: other deps unaffected| F[Inventory Service]
- C (Payment Service) fails, hanging instead of returning errors quickly, perhaps due to a downstream outage of its own.
- B (Order Service) calls C without a tight timeout. Each call to C now blocks for far longer than normal, tying up a thread or connection from B's pool for the duration.
- B's resource pool exhausts. As more requests arrive at B, more threads get stuck waiting on C, until B has no capacity left to serve any request, including ones that don't even touch C.
- A (API Gateway) calls B, and B is now slow or unresponsive for everything, so A's calls to B start timing out or queueing too, degrading A's own capacity in turn.
Worked example: how fast does B's pool actually exhaust?
Little's Law relates the number of requests in flight to the arrival rate and the time each spends being processed:
L=λWSay B receives 500 requests per second, and under normal conditions each call to C takes 50ms:
Lnormal=500×0.05=25 concurrent in-flight requests25 concurrent requests is a light load on a typical connection pool. Now C hangs, and B's HTTP client has no explicit timeout of its own, falling back to a default of 30 seconds:
Lfailure=500×30=15,000 concurrent in-flight requests neededIf B's thread pool has 200 threads, the time to exhaust it entirely is:
texhaust=500200=0.4 sUnder 400 milliseconds. That's how quickly a single hung dependency with no timeout turns into total unavailability for a service handling 500 requests per second: the pool never gets close to steady-state at the 30-second hang time, it simply fills with stuck requests almost instantly and stays full.
Protections at each layer
- At B, calling C: a tight, explicit timeout (measured in low hundreds of milliseconds, not the client library's 30-second default) so a hung call fails fast and frees the thread quickly; a circuit breaker that opens after a run of failures or timeouts, so B stops even attempting calls to C once it's clearly down, and falls back to queueing the order for later processing; a bulkhead, a dedicated connection pool just for calls to C, so exhaustion from C-related calls doesn't consume the threads B needs to serve requests that don't touch C at all (like inventory checks).
- At A, calling B: the same pattern one layer up, a timeout on calls to B, a circuit breaker that trips once B's error rate or latency crosses a threshold, and a fallback (a fast 503 with
Retry-Afterrather than a hung request) so A's own capacity isn't consumed waiting on a B that's already struggling.
Trade-offs & pitfalls
Timeouts that are too aggressive cause false-positive failures under normal, brief latency variance; timeouts that are too loose don't prevent the cascade fast enough, as the Little's Law example shows. Bulkheads cost real resources (a dedicated pool per dependency uses more total connections or threads than one shared pool) in exchange for isolation, so they're worth applying to the dependencies most likely to fail or most likely to take down unrelated traffic if they do. The most common mistake is only protecting the first hop (B to C) and assuming that's sufficient; as the walkthrough shows, without protection at the A to B hop too, the failure still reaches A once B is degraded, just one layer later.
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.