search_ads_recsys / 30 · high availability lesson 30 / 39

High availability & disaster recovery

A recommender that is down serves zero recommendations — which is strictly worse than one that is slightly stale or slightly worse. So the senior design instinct is not "make it perfect," it is "make it degrade, never fail." Availability is a product feature with a number on it, a budget, and a price that climbs steeply for every extra nine.

Availability is a number, and every nine costs money

Lesson 18 got a good model onto the serving path and defended its latency, rollout, and monitoring. This lesson is the reliability layer wrapped around that path: what happens when a dependency dies, a region goes dark, or the feature store hiccups at 2 a.m. The first move is to stop hand-waving "highly available" and write down the actual target, because the entire architecture follows from which nine you are buying.

Availability is uptime fraction A = uptime / (uptime + downtime), and we quote it in nines. The arithmetic is unforgiving — each nine cuts allowed downtime by 10×:

Availability"nines"Downtime / yearDowntime / monthWhat it costs you
99%two nines3.65 days7.2 hOne box, one region, a runbook. Cheap.
99.9%three nines8.8 h43 minRedundant replicas + a load balancer + health checks.
99.99%four nines52 min4.3 minMulti-AZ, automatic failover, no manual step in the loop.
99.999%five nines5.3 min26 sActive-active multi-region, chaos-tested failover, deep pockets.

The cost curve is the whole point. Going from two to three nines is "add a replica." Going from four to five is "run a second region active-active and prove the failover works under chaos" — often a doubling of infra spend and a step-change in operational discipline for ~47 minutes of recovered downtime a year. Chasing an extra nine has steeply rising marginal cost, so a senior engineer asks "what nine does the product actually need?" before architecting. A checkout service may need five; a "people you may know" rail may be fine at three, because its degraded mode (show nobody) is harmless.

SLA vs SLO vs error budget

Three words that get muddled, and the distinction is the spine of an SRE practice:

Latency is an availability concern — a too-slow answer is a failed one
Under a hard timeout, a response that arrives at 800 ms when the budget is 200 ms is indistinguishable from an error — the caller already gave up and served a fallback. So your SLI should count "served within the latency budget," not just "served eventually." This is why the tail-at-scale problem from lesson 18 is an availability bug, not merely a speed one: a fat P99 quietly burns error budget every time a request trips the timeout.

The math that drives every architecture decision

Why does redundancy help and why does a long dependency chain hurt? Two formulas, and they govern everything below.

Series (dependencies in a chain). If a request must traverse components 1…n and every one must succeed, availability multiplies:

Aseries = ∏i=1..n Ai

This is the tyranny of dependencies. Five components each at a respectable 99.9% give an end-to-end 0.9995 ≈ 0.995 — you fell to two-and-a-half nines just by chaining them. Add a sixth and you drop further. The recsys serving path — gateway → candidate generation → feature store → ranker → re-ranker — is exactly such a chain, which is why every hop you can remove from the critical path buys availability directly.

Parallel (redundant replicas). If you run k interchangeable replicas and the request succeeds if any one is up, you multiply the failure probabilities instead:

Aparallel = 1 − ∏i=1..k (1 − Ai)

Two replicas at 99% give 1 − 0.012 = 99.99% — two nines became four by adding one box (assuming independent failures, the catch we return to). This is the entire mathematical case for redundancy: parallelism turns multiplication-of-uptime into multiplication-of-downtime, and downtime is the small number.

The senior synthesis
Architecture is the art of moving components out of the series chain (fewer critical-path hops, more things optional/async) and adding parallel redundancy to the ones that must stay. The graceful-degradation ladder below is a third move the formulas miss: it makes a failed dependency non-fatal by substituting a worse-but-alive answer, which is mathematically like adding a parallel path whose quality is lower but whose availability is ~1.

Redundancy & load balancing — the parallelism, built right

The parallel formula assumes you can actually route around a dead replica. That machinery is stateless replicas behind a load balancer. Stateless is load-bearing: if any replica can serve any request (all state lives in the feature store / cache, not on the box), then the LB can freely spread load and silently drop a sick replica. The instant a replica holds session state, you have lost free failover — that connection's user is stranded.

LB strategyHow it routesWhen it shinesThe catch
Round-robinNext request → next replica, cyclicallyHomogeneous replicas, uniform request costIgnores load — a replica stuck on a slow request still gets its turn
Least-connectionsRoute to the replica with fewest in-flight requestsVariable request cost (some rankings score 1000 candidates, some 50)Needs live connection counts; a just-restarted empty replica gets slammed
Consistent hashingHash(key) → replica, on a ring so adding/removing a node remaps only ~1/k of keysCache / shard affinity — same key always hits the same nodeHot keys create hot nodes; needs virtual nodes to smooth

Why consistent hashing matters for a recommender specifically. The billion-row embedding table of lesson 19 cannot fit on one box — it is sharded. If the routing layer hashes a user/item ID consistently to the same shard, that shard's in-RAM cache of hot embeddings stays warm and the node-local hit rate stays high. With naive round-robin every replica eventually caches everything (memory blowup) or thrashes (cache misses). Worse, when you add or remove a shard, a naive hash(key) mod k remaps almost every key — a cache-cold thundering herd against the backing store. Consistent hashing remaps only the fraction ≈ 1/k that lived on the changed node, so a scale-up is a ripple, not a stampede.

Health checks and N+1 / N+2 capacity

The LB only routes around a dead replica if it knows the replica is dead. Health checks are the mechanism: a shallow check (is the port open?) plus a deep check (can it actually score a synthetic request within budget, with the model loaded and caches warm?). The deep check is what catches a replica that is "up" but serving garbage — the OOM-zombie box that accepts connections and then times out.

Capacity planning is stated as N+1 or N+2: provision enough replicas that you can lose 1 (or 2) and still serve peak traffic within SLA. Run exactly N and the first failure overloads the survivors — who then trip their health checks and get pulled, cascading the whole fleet down. N+2 also covers "one box is down for a deploy when a second one dies."

The retry storm / thundering herd — redundancy's self-inflicted outage
A replica gets slow. Clients time out and retry. Retries are extra load on an already-sick fleet, so more requests slow down, so more clients retry — a positive feedback loop that converts a minor blip into a full meltdown. The same shape appears when a deploy restarts every replica at once (all caches cold simultaneously) or a cache expires en masse. The fixes are mandatory, not optional: exponential backoff with jitter (retry after base · 2attempt ± random — the jitter de-synchronizes clients so they don't all retry on the same tick), a retry budget (cap retries at, say, 10% of requests fleet-wide), and the circuit breaker below. Naive fixed-interval retries are how a one-node hiccup takes down the service.

Multi-region & disaster recovery

A single region is a single failure domain: one fiber cut, one bad config push, one regional power event, and your three-nines-within-the-region service is at zero. Disaster recovery is the layer above redundancy — surviving the loss of an entire region. Two objectives quantify it, and you must state both:

PostureHow it runsRTORPOCost / trade
Active-passiveOne region serves; a standby is kept replicated and idle, promoted on failureMinutes (promotion + DNS/traffic switch + cache warm)Seconds–minutes (async replication lag)Cheaper (standby underused); failover is a tested-rarely event = risky
Active-activeAll regions serve live traffic; a region's loss just sheds its share to the othersSeconds (traffic already flows everywhere; just stop routing to the dead region)Near-zero for reads; writes need conflict handlingExpensive (full capacity in each region, N+1 per region) but failover is the steady state — battle-tested every day

Why a global recsys runs active-active. Three reasons stack. (1) Latency: serve users from their nearest region — the speed-of-light floor on a cross-ocean round-trip alone can blow the budget. (2) Failover confidence: in active-passive the standby path runs only during a disaster — the worst possible moment to discover it doesn't work; active-active exercises every path continuously. (3) The read-heavy embedding table replicates cleanly: embeddings are written by training and read at serving, so each region keeps a read replica of the embedding table (lesson 19) and the inference path is region-local with no cross-region hop. Writes (logging interactions for lesson 17's streaming updates) are funneled or replicated asynchronously, where small lag is tolerable.

Split-brain — the failure that corrupts instead of just stopping
Failover decides "the other region is dead, I'll take over." But what if the other region is not dead — only the network between you partitioned? Now both regions believe they are the sole survivor and both accept writes. That is split-brain: divergent, conflicting state that is far worse than downtime because it's silent corruption you discover later. Defenses: a quorum / consensus protocol (Raft, Paxos — a region needs a majority vote to act as leader, and a minority partition can't get one), fencing (the demoted node is forcibly blocked from writing), and accepting that under CAP you often choose consistency-over-availability for the write path while keeping reads available. Replication lag is the quieter cousin: an async replica is always slightly behind, so a failover to it loses the un-replicated tail (your RPO).

You don't have HA until you've tested the failover

The single most common HA lie is the untested standby. A failover path that has never run in anger will fail in anger — a stale credential, a missing capacity reservation, a DNS TTL nobody lowered. The discipline is failover drills and chaos engineering (Netflix's Chaos Monkey lineage): deliberately kill replicas, regions, and dependencies in production, on purpose, during business hours with everyone watching, and verify the system degrades as designed. GameDays turn the failover from a theoretical capability into a measured, trusted one. If you haven't killed a region on purpose, your RTO is a guess.

Graceful degradation — the recsys-specific superpower

Here is the idea that separates a recommender from a generic web service. A recommender has the rare luxury that a worse answer is almost as good as the right one — a slightly-less-personalized feed still works, where a wrong bank balance does not. So instead of failing when the full model is unavailable, you fall back down a ladder, each rung cheaper, more robust, and less personalized than the one above:

GRACEFUL DEGRADATION LADDER fails over to ↓ when the rung above is sick ┌─────────────────────────────────────────────────────────────┐ │ TIER 0 Full personalized model │ best quality │ real-time features + ranker + re-ranker │ most fragile │ needs: feature store ✓ GPU ranker ✓ ANN index ✓ │ └───────────────────────────┬─────────────────────────────────┘ feature store slow / ranker timeout │ ← circuit breaker opens ┌───────────────────────────▼─────────────────────────────────┐ │ TIER 1 Cached per-user recs │ │ precomputed offline, served from a KV cache │ │ needs: cache ✓ (no live model call) │ └───────────────────────────┬─────────────────────────────────┘ cache miss / cache down │ ┌───────────────────────────▼─────────────────────────────────┐ │ TIER 2 Popularity / trending fallback │ │ top-K by recent engagement, per coarse segment │ │ needs: one small periodically-refreshed list │ └───────────────────────────┬─────────────────────────────────┘ even that store is gone │ ┌───────────────────────────▼─────────────────────────────────┐ │ TIER 3 Static editorial │ worst quality │ a hand-curated list baked into the binary / CDN │ ~never fails └──────────────────────────────────────────────────────────────┘ A ≈ 1

Notice the bottom rung is the popularity prior from lesson 15 — the same maximum-likelihood-under-no-evidence bet that handles a cold user is also your degradation answer for a sick system. That is not a coincidence: "I know nothing about this user" (cold start) and "I can't reach the thing that knows" (degradation) are the same epistemic state, so they share the same fallback. Build the popularity tier once, get both for free.

Circuit breakers — stop hammering the sick service

The mechanism that drives the ladder is the circuit breaker, borrowed from electrical engineering. Wrap each risky dependency in a breaker that watches its error/timeout rate. Three states:

Circuit breakers travel with three companions. Timeouts cap how long any single call may block (no timeout = one slow dependency hangs every thread). Bulkheads isolate resource pools per dependency (like a ship's watertight compartments) so a flood in one — say the ranker's thread pool exhausting — can't sink the gateway that also serves the popularity fallback. Load shedding is the deliberate drop of the lowest-value requests when overloaded — shed the exploration traffic and the logging fan-out before you shed a paying user's core request, keeping the service alive within capacity rather than collapsing under it.

Interactive · availability & degradation calculator

Set each component's availability and how many sit in series on the critical path. Then watch two senior moves rescue it: adding a redundant parallel replica (each component becomes two, by the 1−∏(1−A) rule) and adding a graceful-degradation fallback tier that catches requests the primary path drops. The series multiplication is the villain; redundancy and degradation are the two heroes.

Compose the availability of a serving chain
Per-component availability uses the series rule A=∏Aᵢ. "Add redundancy" doubles each component in parallel (1−∏(1−Aᵢ)). A degradation fallback at availability A_fb rescues the dropped fraction: A_eff = A_chain + (1−A_chain)·A_fb. Watch the warn fire as you chain more dependencies.

end-to-end availability
nines
downtime / year
verdict
Reading

Stateful hazards — where the parallel formula lies

The clean 1−∏(1−A) math assumes independent failures. The dangerous reality is the shared, stateful component that all your "redundant" replicas depend on — a hidden series term that no amount of stateless replication removes. These are the real single points of failure in a recsys:

Interview prompts you should be ready for

  1. "What does 99.99% availability mean in minutes, and why not just always target five nines?" (Four nines ≈ 52 min downtime/year; five nines ≈ 5 min. Each nine cuts downtime 10× but the marginal cost climbs steeply — five nines means active-active multi-region with chaos-tested failover, often a doubling of spend for ~47 min/year. You target the nine the product's degraded mode justifies, not the max.)
  2. "Five services each at 99.9% sit on the request path. What's the end-to-end availability?" (Series rule: availability multiplies, 0.999⁵ ≈ 0.995 — two-and-a-half nines. The lesson is that every critical-path hop multiplies you down, so you remove hops from the chain and add parallel redundancy or a fallback to the ones that remain.)
  3. "How does a recommender degrade gracefully instead of failing?" (A ladder: full personalized model → cached per-user recs → popularity/trending → static editorial, each rung cheaper and more robust. A circuit breaker per dependency opens on a failure-rate spike and drops to the next tier. The popularity tier is the same one cold start uses — "I can't reach the model" and "I know nothing about this user" share a fallback.)
  4. "Explain a circuit breaker and why it beats retries." (Closed → counts failures; Open → stops calling the sick dependency and instantly returns the fallback; Half-open → probes recovery. Retries pile load onto a drowning service (retry storm); the breaker does the opposite — it removes load so the dependency can heal, and gives the caller a fast fallback instead of a timeout. Pair with backoff+jitter, timeouts, bulkheads, load shedding.)
  5. "Active-active vs active-passive, and which for a global recsys?" (Passive: cheaper idle standby, RTO in minutes, async-lag RPO, and a failover path that's only tested during disasters. Active: full capacity per region, RTO in seconds, failover is the daily steady state. A global recsys goes active-active for latency, battle-tested failover, and because the read-heavy embedding table replicates per region cleanly — reads are region-local, writes replicate async.)
  6. "What is split-brain and how do you prevent it?" (A network partition makes both regions think the other is dead; both accept writes and diverge — silent corruption, worse than downtime. Prevent with quorum/consensus leader election (Raft/Paxos — a minority partition can't win a majority), fencing the demoted node from writing, and choosing consistency-over-availability on the write path under CAP.)
  7. "You added redundant ranker replicas but a single feature-store outage still took everything down. Why?" (The parallel-availability formula assumes independent failures; the feature store is a shared stateful SPOF that all 'redundant' replicas depend on — a hidden series term. You parallelized the cheap stateless part and left the expensive shared dependency single. Fix: replicate the store and put a circuit breaker in front so a sick store drops to cached/popularity tiers instead of hanging every replica.)
  8. "How do you actually know your failover works?" (You don't until you've run it. Failover drills / chaos engineering — kill replicas, dependencies, and whole regions in production on purpose during business hours and verify the system degrades as designed. An untested standby fails in anger; if you haven't killed a region deliberately, your RTO is a guess.)
Takeaway
A down recommender serves zero recs — worse than a stale or worse one — so the goal is degrade, never fail. Availability is a budgeted number: each nine costs 10× more for 10× less downtime, so target the nine the product's degraded mode justifies. Series dependencies multiply availability down (∏Aᵢ); redundancy multiplies downtime down (1−∏(1−Aᵢ)); and a graceful-degradation ladder — full model → cached → popularity (the cold-start prior) → static editorial, driven by circuit breakers — makes a dead dependency non-fatal. Go active-active multi-region for latency and battle-tested failover, guard the write path against split-brain with quorum + fencing, and remember the parallel math lies whenever a shared stateful SPOF (feature store, registry, cache) hides in the chain. You don't have HA until chaos engineering has proven the failover in production.