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 bulkhead pattern, and how does it stop one failing dependency or noisy tenant from taking down the whole system? Give a concrete example of where you'd draw the isolation boundary.
Sample Answer
Direct answer
The bulkhead pattern partitions a system's resources (thread pools, connection pools, CPU, or entire nodes) into isolated compartments, named after a ship's watertight bulkheads, so that one failing dependency or one noisy tenant can only exhaust the resources in its own compartment, not the resources every other caller depends on. Without bulkheads, a single slow or misbehaving dependency can consume every available thread or connection in a shared pool, and a completely healthy code path fails simply because it couldn't get a thread to run on.
Where to draw the isolation boundary
A concrete example: an API gateway calls three downstream services, an inventory service, a recommendations service, and a payments service, all through one shared thread pool. If recommendations starts responding slowly, every thread in the shared pool eventually ends up blocked waiting on recommendations calls, and inventory and payment requests start timing out too, even though nothing is wrong with either of them. The fix is a dedicated, bounded thread pool (or connection pool) per downstream dependency: recommendations gets its own pool of, say, 10 threads, so a recommendations outage can stall at most those 10 threads and its own queue, while inventory and payments keep running normally on their own separate pools.
The boundary should sit wherever one caller's failure or slowness shouldn't be able to spill onto another caller's request. Common places to draw it:
- Per-downstream-dependency, as in the example above: each external service or database gets its own pool so a slow one can't starve calls to a fast one.
- Per-tenant, in a multi-tenant system: each tenant (or tenant tier) gets a capped share of connections or CPU so one noisy or abusive tenant can't degrade service for everyone else on shared infrastructure.
- Per-criticality-tier: payment and auth paths get reserved capacity separate from lower-priority paths like analytics or notifications, so a spike in low-priority traffic can't crowd out the paths that actually matter.
Trade-offs & pitfalls
Bulkheads trade utilization for isolation: reserved capacity that a compartment isn't currently using sits idle rather than being available to a busier compartment, so a poorly sized bulkhead can cause localized throttling even while the system as a whole has spare capacity. Sizing is the actual hard part in practice, not the pattern itself: too small and a legitimate burst of normal traffic gets rejected by its own bulkhead; too large and the isolation becomes theoretical, because if every pool is sized close to the shared pool's original total, a single compartment can still consume enough of the machine's real resources (CPU, memory, file descriptors) to degrade its neighbors even though the pool counters look fine. Bulkheads are also a different tool from a circuit breaker and the two are frequently confused: a bulkhead limits how much of a shared resource one dependency can consume (a capacity boundary), while a circuit breaker stops sending requests to a dependency once it's clearly failing (a decision to stop calling at all); they're complementary, since the bulkhead caps the damage while the circuit breaker is deciding whether to keep trying, and production systems typically use both on the same dependency together. The same reasoning extends beyond web request threads: an ML-serving platform running GPU inference for multiple models on shared hardware applies the identical idea by pinning each model (or tenant) to a dedicated slice of GPU memory and compute, so one model that starts issuing runaway-batch-size requests can't starve GPU capacity away from every other model sharing that hardware.
Staff-level: propose an enterprise resilience strategy for handling dependency failures across hundreds of services and multiple third-party APIs. Cover reusable patterns, governance, telemetry, runbooks, and how you'd prioritize the fastest reduction in customer impact given an existing high-MTTR baseline.
Sample Answer
Direct answer
At hundreds-of-services scale, resilience can't be a per-team decision made independently for each service, because the inconsistency itself becomes the risk: one team's excellent circuit-breaker tuning doesn't help if the team three hops upstream never implemented one at all. The strategy has four layers that reinforce each other: reusable patterns shipped as shared libraries so teams don't reinvent (and mis-implement) the same primitives, governance that makes resilience a gate rather than a suggestion, telemetry that gives every team and the org as a whole a common picture of dependency health, and runbooks that turn "we know this failure mode exists" into "here's exactly what an on-call engineer does about it at 3am." Given an existing high-MTTR baseline, the fastest reduction in customer impact rarely comes from writing more resilience code first; it comes from instrumenting what's actually failing today and fixing the highest-blast-radius gaps, because at this scale intuition about "which dependency is riskiest" is usually wrong.
Reusable patterns
Ship standardized client libraries (one per major language in use) that implement the standard resilience toolkit for calling another service: something to stop hammering a dependency that's already failing, something that spaces out and caps retries so they don't pile on, something that enforces how long a call is allowed to wait, and something that stops one slow dependency from exhausting resources other calls need. (Named precisely, for readers who want the specific patterns: circuit breakers, jittered exponential backoff with retry budgets, timeout and deadline propagation, and bulkhead-style connection pooling.) Ship these as versioned SDKs or a sidecar proxy (a small helper process deployed alongside the service, in its own container but sharing the same host or pod, that intercepts outbound network calls and applies the retry/timeout/circuit-breaker logic on the service's behalf, so the service's own code doesn't have to implement the pattern itself) for languages that can't easily share a library. The goal isn't just code reuse, it's that every team's circuit breaker behaves the same way under the same conditions, so an incident responder who understands one service's failure behavior can reason about any other service's failure behavior too. A pattern catalog with runnable examples for each pattern (service-to-service versus third-party dependency, since third parties often need more conservative defaults) makes correct usage the path of least resistance.
Governance
| Mechanism | What it enforces |
|---|---|
| SLI/SLO declaration per service (SLI: the metric you measure, like latency or success rate; SLO: the target you commit to for that metric) | Every service that calls another declares what it needs (latency, success rate) from that dependency, making implicit expectations explicit and auditable |
| Architecture review gate | New services and new external integrations pass a resilience checklist (timeouts set, retries bounded, circuit breaker present) before launch, not retrofitted after an incident |
| Third-party vetting checklist | New vendor integrations are assessed for SLA terms, documented retry/rate-limit behavior, and escalation contacts before the integration ships, since a third-party outage is not something your own SDK can fully protect against |
| CI-enforced lint rules | Timeouts, bounded retries, and SLO annotations are checked automatically at merge time, catching the class of bug where a developer forgot a timeout entirely rather than relying on code review to catch it |
Telemetry
Every SDK emits standardized telemetry (circuit-breaker state transitions, retry counts, latency histograms, per-dependency error codes) into a shared observability stack, feeding a small number of org-wide dashboards: a dependency heatmap showing which services are the riskiest single points of failure by fan-in (the number of other services that call into this one; a high fan-in means many things break at once if it fails), a per-service SLO burn-rate view, and a "top failing third parties" view that surfaces vendor issues before they've caused five separate team-level incidents that nobody connected. Alerting on SLO burn rate and on sudden spikes in open-circuit count catches emerging problems before they cascade, rather than after an incident is already customer-visible.
Runbooks
Per-dependency runbooks covering detection, mitigation (force-close or force-open a circuit, apply emergency throttling, shift traffic to a degraded mode), rollback, and escalation contacts, with the common actions automated (a CLI or button to toggle a circuit breaker, not a manual code deploy) so response time doesn't depend on someone remembering the right kubectl incantation under pressure. Runbooks that are only tested during real incidents are unreliable; quarterly tabletop exercises (a facilitated walkthrough where the on-call team talks through their response to a scripted incident scenario out loud, step by step, without touching any real production system) simulating a specific third-party outage validate that the documented steps actually work and that the on-call rotation knows where to find them.
Prioritizing remediation against a high-MTTR baseline
Given limited engineering time, the fastest reduction in customer impact comes from ranking services by (fan-in × current failure rate × missing-resilience-pattern count), not by which team is loudest or which service feels intuitively risky. Tracing the formula through a small, illustrative example makes the ranking concrete: Service X has fan-in 50, a current failure rate of 2% (0.02), and 3 missing resilience patterns, scoring 50×0.02×3=3.0; Service Y has fan-in 5, a much higher failure rate of 10% (0.10), but only 1 missing pattern, scoring 5×0.10×1=0.5; Service Z has fan-in 200, a low failure rate of 0.5% (0.005), and 2 missing patterns, scoring 200×0.005×2=2.0. Ranked by score, the fix order is X (3.0), then Z (2.0), then Y (0.5), even though Y's raw failure rate is the highest of the three, because Y's small blast radius (only 5 callers) and already-thin gap list make it a low-leverage fix by comparison. A service with 50 upstream callers and no circuit breaker is a much higher-leverage fix than a service with 2 callers and a full resilience suite already in place, even if the second service "feels" more important because it's customer-facing. Concretely: instrument telemetry first (you can't rank what you can't see), fix the highest fan-in gaps first (biggest blast-radius reduction per engineering-hour), and treat "ship the shared SDK" and "mandate its use via the architecture-review gate" as sequential, not simultaneous, since a library nobody's required to adopt doesn't move the MTTR number regardless of how good it is.
Trade-offs & pitfalls
Uniform SDK adoption is the ideal, but legacy or polyglot systems make a single shared library impractical everywhere; a sidecar-proxy approach extends the same governed behavior to services that can't easily embed the SDK, at the cost of an extra network hop and an additional piece of infrastructure to operate. Mandating resilience patterns through architecture-review gates works for new services but does nothing for the hundreds of already-shipped services that predate the gate, so rollout has to include a deliberate retrofit plan (again, prioritized by the fan-in ranking above) rather than assuming the gate alone will fix the fleet over time. The biggest governance failure mode at this scale is treating the checklist as a one-time approval rather than a continuously monitored property. A service that passed its architecture review with a correctly-configured circuit breaker two years ago can silently regress (a config change, a library version bump that changed defaults) with nobody noticing until the next incident, which is exactly what the telemetry layer's continuous SLO-burn alerting is meant to catch that a point-in-time review cannot.
When should failover be fully automated versus require a human to approve it? Walk through the factors that push you toward one or the other.
Sample Answer
Direct answer
Automate failover when the detector is high-precision, the failover action is reversible and idempotent, and the cost of a wrong automatic trigger is bounded and recoverable. Require a human when any of those breaks down, especially when a wrong trigger risks unrecoverable data divergence or an irreversible action. Expected-value math on detection accuracy alone favors automation more than intuition suggests, but it is reversibility and blast radius, not raw precision, that should gate the decision.
Structured elaboration
| Factor | Pushes toward automation | Pushes toward manual approval |
|---|---|---|
| Detection precision | High, multi-signal, correlated | Single noisy signal, history of false positives |
| Reversibility of the action | Fully reversible, idempotent | One-way (data promotion, DNS cutover with no clean undo) |
| Blast radius of a wrong trigger | Isolated to one service or region | Cross-service, cross-customer, or financial |
| Data consistency risk | Stateless or conflict-free (CRDT, idempotent) | Risk of split-brain (two nodes each independently believing they are the current leader, and both accepting writes at the same time, so the data silently diverges) or a double write |
| Regulatory or audit requirement | None, or satisfied by an audit log | Explicit approval-before-action mandate |
| Operational maturity | Tested runbooks, regular chaos drills | First time this failover path has been exercised |
A hybrid middle ground. Mature systems rarely pick one point on the automate-versus-manual spectrum. They tier it: automated detection and containment (circuit breakers, traffic throttling) run automatically because those actions are cheap to reverse, while the highest-blast-radius action (full regional failover, promoting a new primary) goes through an automated-detect, human-approve gate with an escalation timeout if nobody responds.
flowchart TD
A[Alert fires] --> B{Multi-signal, high-precision detector?}
B -->|No| M[Manual: page human, human confirms before failover]
B -->|Yes| C{Action reversible and idempotent?}
C -->|No| H[Hybrid: auto-detect and auto-contain, human approves full failover]
C -->|Yes| D{Wrong trigger risks split-brain or data loss?}
D -->|High risk| H
D -->|Low risk| E[Automate: auto-detect and auto-failover with fencing token and audit log]
A fencing token here is a number that increases with every failover action; if a stale, already-superseded actor (an old primary that thinks it's still in charge, for example) tries to act after a newer one has taken over, its writes carry an outdated token and get rejected, so a late-arriving action from a process that no longer should be acting can't silently corrupt state.
Framing it as expected value. For a given alert, the expected value of automatic failover is:
EVauto=p×value saved by faster RTO−(1−p)×cost of a false triggerwhere p is the detector's precision, the probability an alert reflects a real failure.
Worked example
Assume correct auto-failover cuts RTO from a 15-minute human-paged response to a 2-minute automatic one, a 13-minute improvement, against a downtime cost of $50k/hour:
value saved per true incident=6013×50,000=10,833A false trigger causes roughly 3 minutes of avoidable disruption (connection draining and reconnect storms) at the same rate:
cost per false trigger=603×50,000=2,500At a detector precision of p=0.9:
EV=0.9×10,833−0.1×2,500=9,750−250=9,500Solve for the breakeven precision where EV=0:
p×10,833=(1−p)×2,500 p=10,833+2,5002,500≈0.19Pure expected value favors automation down to a detector that is right only 19% of the time, far noisier than any detector actually deployed. That is the point: raw EV almost always says automate. The equation treats every false trigger as a bounded $2,500 cost, which is only true if the action is reversible. If a wrong trigger can cause split-brain or an irreversible data promotion, the real cost of that tail case is not in the equation at all, which is why reversibility, not precision, is the dominant factor in practice.
Trade-offs & pitfalls
- The most common wrong turn is optimizing for detector precision and stopping there; a 99%-precision detector triggering an irreversible action is still a bad automation candidate if the 1% case is catastrophic.
- Automating containment (throttle, circuit-break) before automating the full failover captures most of the RTO benefit with much lower blast radius; teams often skip straight to automating the whole failover and take on risk they did not need.
- An approval gate with no timeout just becomes a slower manual failover with extra steps; if a human stays in the loop, define an explicit escalation timeout.
- Chaos-testing the automated path before trusting it in production is not optional. An automation that has never been exercised against a real failure is a new, untested failure mode, not a safety net.
Explain active-active versus active-passive architecture. For each, walk through the typical failover behavior, what it takes to detect a failure, and when you'd choose one over the other.
Sample Answer
Active-active runs all nodes or regions serving live traffic at the same time, so failover is mostly a routing problem: stop sending traffic to the unhealthy node. Active-passive keeps one side idle (or partially warmed) as a standby, so failover is a promotion problem: detect the primary is down, make the standby the new primary, then redirect traffic to it. That difference in what has to happen during failover is what drives everything else: recovery speed (RTO, recovery time objective: how long it takes to restore service after a failure), data-consistency risk (RPO, recovery point objective: how much data, measured in time, you could lose in a failure), and cost.
Comparing the two
| Dimension | Active-active | Active-passive |
|---|---|---|
| What's serving traffic | All nodes/regions, concurrently | Only the primary; standby is idle or warm |
| Failure detection | Health checks per node feed a load balancer or GSLB (Global Server Load Balancer: a DNS-based load balancer that routes traffic across regions, not just across servers in one place), which simply stops routing to the failed one | Health checks must trigger an explicit promotion decision, usually with a consensus/quorum step to avoid promoting during a false alarm |
| What "failover" does | Reroute traffic; no state transition needed | Promote replica to primary, update routing (DNS or LB config), then reroute traffic |
| Typical RPO | Near-zero if writes are synchronously replicated or conflict-resolved; otherwise bounded by replication lag | Near-zero with a synchronous standby, up to minutes with an async one |
| Consistency risk | Needs conflict resolution or partitioned ownership if writes happen on both sides (dual-writer problem) | Simpler: single writer at any point in time, no conflict resolution needed |
| Cost/complexity | Higher: full capacity running everywhere, plus distributed-write tooling | Lower: standby can run at reduced capacity (or be provisioned only at failover time) |
| Best fit | Latency-sensitive, globally distributed traffic; teams that can invest in multi-writer data patterns | Systems needing a single source of truth for writes (most transactional relational databases); cost-sensitive setups |
Worked example: deriving RTO for each
Pin the same detection policy to both: a health check runs every 5 seconds, and 3 consecutive failures are required before the system acts (15 seconds to declare a node down; this avoids reacting to a single dropped probe).
Active-active RTO: once the node is declared down, the load balancer or GSLB removes it from rotation immediately (no promotion step). If we assume that rotation update takes about 5 seconds to propagate to all edge/LB nodes:
RTOactive-active≈15s (detect)+5s (reroute)=20 secondsActive-passive RTO: the same 15 seconds to detect, plus a promotion step (electing the standby, replaying any un-applied log entries, opening it for writes, say 20 seconds for a warm standby with low replication lag) plus DNS or routing propagation (assume a low TTL of 30 seconds, and worst case the full TTL has to expire before every client picks up the change):
RTOactive-passive≈15s (detect)+20s (promote)+30s (routing propagation)=65 secondsUnder these pinned assumptions, active-active recovers about 3x faster, entirely because it skips the promotion step, and that gap only grows if the standby is cold rather than warm (add provisioning time) or if the routing layer is DNS with a high TTL instead of a fast health-checked LB.
Trade-offs and pitfalls
Active-active's speed advantage is real but not free: the moment two regions can both accept writes to the same record, you have a distributed-write problem, and skipping it (assuming replication will "just" reconcile) is the most common wrong turn. It needs either a conflict-resolution strategy (last-write-wins with vector clocks, CRDTs) or partitioned ownership (each region owns a disjoint key range) to avoid silently losing or corrupting data during a partition. Active-passive's failure mode is different: over-eager health checks or a flapping network link can trigger a premature promotion while the old primary is still technically reachable by some clients, producing two nodes that both believe they're primary (split-brain), which is why production active-passive systems add fencing (forcibly cutting off the old primary's access to shared storage) on top of the detection logic above, not just a timer. The same trade-off shows up outside a classic web/DB stack too: failing over an ML-serving fleet is closer to active-active in spirit (multiple replicas of the same model serving concurrently, so losing one just drops capacity) unless the model itself is being hot-swapped, in which case the promotion-style risk (serving from a half-loaded or stale model version) reappears.
Walk through full, incremental, differential, and snapshot-based backups. For a large transactional database, which combination would you actually run, and what does each choice cost you in restore time versus storage?
Sample Answer
Direct answer: A full backup copies everything; an incremental backup copies only what changed since the last backup of any kind; a differential backup copies everything changed since the last full backup; and a snapshot captures a point-in-time, storage-level image (often copy-on-write) rather than a separate file copy. For a large transactional database, the common answer is a weekly full plus daily incrementals plus continuous transaction-log shipping, because that combination gives a low recovery point (minutes of data loss) without paying full-backup storage and I/O cost every day.
Structured elaboration
| Backup type | What it stores | Restore chain length | Storage growth pattern |
|---|---|---|---|
| Full | Everything, every time | 1 backup set | Largest per run; constant regardless of how much data changed |
| Incremental | Changes since the last backup (full or incremental) | Full + every incremental since it, in order | Smallest per run, but restore requires replaying the whole chain |
| Differential | Changes since the last full | Full + latest differential only | Grows every day until the next full, then resets |
| Snapshot | Point-in-time storage image (usually copy-on-write) | 1 snapshot (or a base + deltas depending on the storage engine) | Cheap to create, but retaining many snapshots long-term accumulates the same changed-block cost as incrementals |
Restore chain length is the right way to reason about recovery complexity without making an unverifiable wall-clock claim: full and snapshot restores involve one artifact; differential restores always involve exactly two (full + latest differential, regardless of how many days have passed); incremental restores involve as many artifacts as days since the last full, so a chain 6 days deep means 7 total pieces (1 full + 6 incrementals) must apply cleanly, and a single corrupted link breaks the whole chain.
Worked example: 5 TB transactional database, weekly full + daily incrementals
Pin the assumption explicitly since this has to be derived, not asserted: assume 2% of the database's data changes per day (an illustrative rate; the real number should come from measuring actual write volume, but 2%/day is a reasonable planning figure for a moderately active OLTP (online transaction processing: a system handling many small, frequent reads and writes, like order or payment records, as opposed to bulk analytics queries) system).
- Day 0: full backup = 5 TB.
- Each daily incremental ≈0.02×5 TB=100 GB (treating the changed-data fraction as roughly constant day to day, a simplification for this estimate).
- By day 6 (just before the next weekly full), total incremental storage accumulated is 6×100 GB=600 GB.
- Total storage footprint for that week's backup set: 5 TB+0.6 TB=5.6 TB, a 12% overhead over the full alone (5.6/5=1.12).
Compare to a differential-only strategy at the same 2%/day rate: day 6's differential (changes since the day-0 full) would also be roughly 600 GB if changes were non-overlapping, but in practice differentials tend to be larger than the sum of same-period incrementals, because a row updated on day 2 and again on day 5 shows up in every day's differential from day 2 onward but only once across the incrementals. So the "incremental is smaller in total storage, differential is smaller to restore" trade-off holds here as expected.
Recovery point: with only daily backups, the recovery point objective (RPO) is bounded by the backup interval: worst case, you lose up to 24 hours of data (a failure right before the next scheduled backup). Adding continuous transaction-log shipping on top of the daily incrementals tightens the RPO to roughly the log-shipping interval (commonly seconds to a few minutes), independent of the backup schedule, which is why "backups alone" and "backups plus log shipping" are different RPO conversations for a transactional system.
Trade-offs & pitfalls
- Incrementals minimize storage and per-run I/O but maximize restore complexity (more pieces that must all be intact and applied in order); a single corrupted incremental in the chain can break every restore point after it.
- Differentials trade some storage growth (they get bigger every day until the next full) for a simpler, faster-to-verify two-piece restore.
- Snapshots are excellent for fast recovery when the underlying storage supports them cheaply, but for a live transactional database they require application-consistent quiescing (flushing buffers, pausing writes, or using the database's own snapshot-consistency mechanism) or the snapshot can capture a torn, inconsistent state.
- A common wrong turn: treating "we take backups" as equivalent to "we can restore." The only real validation is a periodic restore drill that rebuilds the database from the backup chain end to end; storage-level backup success says nothing about whether the restore path actually works.
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.