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.
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.
How would you structure a DR testing program over a year: what mix of tabletop exercises, partial failover drills, and full failover tests would you run, and how often? How do you know a test actually validated your RTO/RPO rather than just checking a box?
Sample Answer
Direct answer
Structure the program as a pyramid: frequent, cheap, low-blast-radius tests at the base (tabletop exercises and small chaos experiments) and rare, expensive, high-fidelity tests at the top (a full regional failover), with the mix and cadence driven by how critical the system is. A test only "validates" RTO/RPO if it measures the actual cutover duration and actual data-loss window against the stated objectives; a test that only checks "the failover script exited 0" validates nothing about either number.
Program cadence
| Test type | Frequency | Scope | Blast radius | What it validates |
|---|---|---|---|---|
| Tabletop exercise | Monthly | Walk through the runbook verbally with the team, no systems touched | None | Runbook completeness, team knowledge, communication plan |
| Chaos experiment | Monthly (staggered by service) | Targeted fault injection (latency, instance kill) in staging or a canary slice of production | Small, scoped | Individual resilience mechanisms (timeouts, retries, circuit breakers) |
| Partial failover drill | Quarterly | One tier or one region's traffic for a limited cohort | Medium | Actual RTO/RPO for a real subsystem, under real (if partial) load |
| Full failover rehearsal | Semi-annually or annually | Entire production stack cut over to the DR target | Large, scheduled maintenance window | End-to-end RTO/RPO for the whole system, including cross-service dependencies and data reconciliation |
Critical, revenue-impacting systems get more frequent partial drills and at least one full rehearsal a year; lower-tier systems can run on tabletop plus chaos testing alone, escalating to a partial drill only if the tabletop surfaces a gap worth verifying live.
Knowing a test actually validated RTO/RPO
Every drill needs three explicit, pre-committed numbers before it starts: the RTO objective, the RPO objective, and how each will be measured (timestamp of last successful replication for RPO, timestamp from failure detection to traffic fully serving from the target for RTO). A test that "succeeded" without producing those two measured numbers against those two objectives is a box-check, not a validation, no matter how smoothly it went operationally.
Worked example
Take a service with an RTO objective of 120 minutes and an RPO objective of 15 minutes. During a quarterly partial failover drill, the team explicitly instruments the cutover:
- Failure is injected at T0. Traffic is fully migrated and serving correctly from the DR target at T0+95 minutes.
- The last successful replication timestamp before the failure was 12 minutes prior to injection.
Both are checked against the objectives directly, not inferred:
- RTO: 95 minutes measured, against a 120-minute objective. Met, with 25 minutes of margin.
- RPO: 12 minutes of un-replicated data, against a 15-minute objective. Met, with 3 minutes of margin.
If instead the drill log only said "failover completed successfully" with no cutover timestamp and no replication-lag reading at the moment of injection, there is no way to know whether either objective was actually met; the drill exercised the mechanics but validated neither number. That distinction, an explicit measured duration and lag compared against a stated objective versus a pass/fail exit code, is what separates a real validation from box-checking.
Data-pipeline experiments: staging first, then guarded production
For data pipelines specifically, the failure modes worth testing (replication lag, partition loss, reprocessing correctness) are riskier to inject directly into production because a botched experiment can corrupt or duplicate real data, not just cause temporary unavailability. The staging-first policy: run the same fault injection against a staging environment fed by a realistic (sampled or replayed) data volume first, validate the pipeline's idempotency and replay logic there, and only graduate to a production experiment once staging has proven the recovery path is safe, and then only against a bounded, reversible slice (a single partition or a single non-critical topic) with a documented rollback.
Trade-offs & pitfalls
Full rehearsals give the highest-fidelity validation but are expensive in engineering time and carry real operational risk, so they can't run monthly; the tabletop and chaos-testing layers exist precisely to catch cheap, obvious gaps before they ever reach a full rehearsal. The most common pitfall is a program that runs consistently but never increases rigor: five consecutive tabletop exercises that always conclude "the plan looks fine" without ever executing a real cutover leaves the actual RTO/RPO numbers unverified. The other common trap is running the full rehearsal on a quiet, low-traffic weekend that doesn't resemble real peak load, which can pass a drill that would fail under the conditions an actual disaster is most likely to occur alongside (peak traffic, a concurrent incident, or a partially degraded starting state).
Most of your traffic is reads, but you occasionally get writes from any region, and you want to route reads to the nearest region for latency. Walk through the replication and consistency strategy that makes this work.
Sample Answer
Direct answer
Deploy a read replica in every user-facing region and route reads to the nearest one for latency, while anchoring writes to a single-writer-per-shard model: each account or entity has one home region that owns writes for it (sharded by key, not globally centralized), and a write originating from any other region gets forwarded to that entity's home region. Reads stay fast everywhere because they never leave the local region; writes pay a forwarding cost only when they originate somewhere other than the entity's home region, which for most workloads is the minority case.
Architecture
flowchart TD
CLIENT[Clients worldwide] --> RA[Region A read replica]
CLIENT --> RB[Region B read replica]
CLIENT --> RC[Region C read replica]
RA --> FWD[Write forwarder]
RB --> FWD
RC --> FWD
FWD --> LEADER[Anchor write leader, sharded by key range]
LEADER --> CDC[CDC stream]
CDC --> RA
CDC --> RB
CDC --> RC
- Read routing: DNS-based or edge-proxy latency routing sends each client to its nearest region; that region serves reads from its local replica, optionally backed by a local edge cache for hot keys to cut load further.
- Write routing: a lightweight write-forwarder in each region inspects the target entity's shard key, determines which region owns it, and forwards the write there if it isn't local; the write commits in its home region and the forwarder returns the result (or an idempotency-tracked async acknowledgment) to the originating client.
- Replication: the write's home region streams committed changes via change-data-capture (CDC) to every read replica, asynchronously; replicas apply changes in the order the CDC stream delivers them, tracking a per-record last-writer timestamp so replay order and true causal order stay consistent.
Consistency model
This design is eventually consistent for reads: a read served from Region B immediately after a write committed in Region A's home shard may not reflect that write yet, bounded by CDC replication lag rather than by any hard guarantee. That's an explicit, necessary trade for the latency goal, since making every read wait for a cross-region round-trip to confirm it has the absolute latest value would defeat the entire point of routing reads to the nearest region. Where a specific read genuinely needs to see its own very-recent write (a user immediately viewing an item they just created), the standard fix is read-your-own-writes: route that specific read to the entity's home region (or to a replica known to have caught up past a specific CDC watermark) instead of the nearest replica, rather than weakening the consistency model for every read to satisfy the rare case.
Write conflicts are structurally rare by design, because each entity has exactly one home region and therefore exactly one writer at any time; there's no multi-master merge problem to solve because there's no multi-master. The forwarding hop is the cost of ruling that problem out entirely rather than solving it after the fact.
Worked example: tracing one write end to end
Pin a concrete case: user_id=482's home region is us-east (that's where its shard's writer lives), but the client happens to be connected to the nearest edge in eu-west. The eu-west write-forwarder inspects the shard key for user_id=482, sees it belongs to us-east, and forwards the write there; assume a cross-region round trip of about 90ms for that forward, so the write is accepted and committed in us-east roughly 95ms after the client sent it (90ms network plus a small local processing cost). From there, the CDC stream carries that committed change out to every read replica asynchronously; assume that hop adds about 300ms before eu-west's local replica has applied it (its own network hop plus normal stream batching, separate from the synchronous forward that carried the write there). So the total time from write-acceptance to eu-west's replica reflecting the change is about 95ms+300ms=395ms, call it roughly 400ms. A read served from eu-west's local replica 1 second (1,000ms) after the write was accepted already reflects it, comfortably past the ~400ms it takes to land; a read served only 50ms after acceptance would not yet reflect it, since 50ms is well inside that ~400ms propagation window, which is exactly the read-your-own-writes gap the design accepts and works around by routing that specific kind of read to the home region instead.
Trade-offs & pitfalls
The biggest latency cost this design accepts is on writes that originate far from an entity's home region: a user in Region C writing to an entity whose home shard is in Region A pays a full cross-region round trip for that write, even though every other user's reads and most other writes stay fast. If write locality doesn't naturally match user geography (an entity created in one region gets written to mostly by users somewhere else over time), this cost compounds instead of amortizing away, which is worth checking against real traffic patterns before committing to a static shard-to-region mapping. Replication lag is the other pitfall: CDC-based replication is asynchronous by nature, so a region that falls behind (network partition, replica overload) serves increasingly stale reads without necessarily surfacing an error, which is why lag needs to be an actively monitored metric with alerting, not just an assumed-small property of the pipeline. Finally, resist the temptation to solve the "occasional write from any region" requirement with full multi-master writes accepted locally everywhere; that reintroduces exactly the conflict-resolution complexity (concurrent writes to the same entity from two regions, needing merge logic or last-write-wins with its own correctness risks) that single-writer-per-shard was specifically chosen to avoid, in exchange for a write-latency win that the stated 90-percent-read workload doesn't actually need.
Using the CAP theorem, walk through the trade-off you'd make for a financial ledger service versus an analytics event aggregator. Which guarantee does each give up during a network partition, and why?
Sample Answer
CAP theorem says that during a network partition, a distributed system must choose between consistency (every read sees the latest acknowledged write) and availability (every request gets a response), because it can't guarantee both while the partitioned halves can't talk to each other. Partition tolerance itself isn't optional for any system that spans more than one node, so the real choice interview questions are testing is CP versus AP, and the right answer depends entirely on what happens if you get it wrong.
Financial ledger: choose CP
A ledger getting a stale or divergent balance is a correctness bug with real financial consequences (double-spend, incorrect balance shown, a transaction accepted twice), so consistency has to win. The concrete mechanism is a consensus protocol requiring a write quorum.
Deriving what "sacrifice availability" actually means, with a pinned example: take a 5-node replica set (a typical Raft cluster size) where a majority quorum is required to commit a write:
quorum=⌊2N⌋+1=⌊25⌋+1=3That cluster tolerates up to N−quorum=2 node failures while still committing writes. Now suppose a network partition splits the 5 nodes into a 3-node side and a 2-node side. The 3-node side still has a majority (3 ≥ quorum of 3), so it keeps accepting writes and stays both consistent and available. The 2-node side does not have a majority (2 < 3), so by design it must refuse writes, becoming unavailable, specifically to prevent both sides from independently committing conflicting transactions. That refusal on the minority side, not a global shutdown, is the literal, computable meaning of "CP sacrifices availability during a partition": only the minority partition goes unavailable, and only for writes.
Analytics event aggregator: choose AP
An analytics pipeline getting an event a few seconds late, or briefly double-counted before deduplication catches up, is a rounding error on a dashboard, not a financial loss, so availability wins: every node keeps ingesting even when it can't see the others.
The mechanism looks different from the ledger: instead of a write quorum gatekeeping every write, use leaderless or partitioned ingestion (each partition or node accepts writes for its own shard independently, as in Kafka-style partitioned logs or a Dynamo-style leaderless store), so there's no majority to lose and no write path that can be blocked by a partition. The cost shifts from "unavailable during a partition" to "eventually reconciled after one": deduplication on ingest, idempotent consumers, and background reconciliation jobs to merge whatever diverged while the partition was open.
Comparing the two designs
| Dimension | Ledger (CP) | Analytics aggregator (AP) |
|---|---|---|
| Write path | Quorum-gated (majority must ack) | Leaderless / partitioned, no quorum gate |
| During a partition | Minority side refuses writes | Both sides keep accepting writes independently |
| Consistency mechanism | Consensus (Raft/Paxos-style) | Eventual consistency + reconciliation |
| Failure mode if you pick the wrong side | Double-spend, incorrect balances | Stale dashboard, temporary undercounts |
| Client-side pattern needed | Idempotency keys so a client retry after a rejected write is safe | Deduplication keys so late/duplicate events don't double-count on reconciliation |
Trade-offs and pitfalls
The common misreading of CAP is treating CP as "the whole system goes down during any partition," when the quorum math above shows it's specifically the minority side, and only for writes, reads can often still be served (possibly stale, depending on the read-consistency level chosen). The common mistake on the AP side is stopping at "it's eventually consistent" without actually building the reconciliation path: leaderless ingestion without idempotent consumers and deduplication just relocates the correctness problem downstream instead of solving it. And even a CP ledger still needs idempotency keys on the client side: a client that times out waiting for a quorum ack and retries the same transaction must not have it applied twice, which is a consistency concern CAP itself doesn't cover but that any real ledger design has to handle regardless of which side of CAP it lands on.
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.