TL;DR

1. The problem: why do systems need resilience patterns?

Opening the Instagram home feed looks like one request, but behind the API Gateway the News Feed Service fans out to many others: the feed cache (Redis), the ranking service, the post metadata store, the like & comment counters, the user service for profile pictures, the Stories tray, ads, and more. Each of those talks to its own databases and caches.

Now do the arithmetic. Suppose each of 10 dependencies is independently available 99.9% of the time. If the feed needs all of them:

P(feed works) = 0.999^10 ≈ 0.990  → 99.0% — about 7 hours of failure per month
With 30 dependencies: 0.999^30 ≈ 0.970  → 97% — the app is broken ~22 hours/month

Adding dependencies multiplies failure probability. At scale, with tens of thousands of servers, hardware failure is not an exception — it is a constant background rate. Disks die daily, a top-of-rack switch fails weekly, a deploy introduces a bug, a garbage-collection pause freezes a JVM for 2 seconds. The goal of resilience engineering is to make the system degrade gracefully: a failure in the like counter should mean "like counts are missing for a minute", not "Instagram is down". (See availability and reliability for the definitions of nines.)

🧠 Analogy A house's electrical system has a circuit breaker for each room: a short circuit in the kitchen trips the kitchen breaker and the rest of the house keeps its lights on. Ships are divided by watertight bulkheads so one hole floods one compartment, not the whole hull. A restaurant that's overwhelmed stops seating new guests (load shedding) rather than letting the kitchen collapse and serving nobody. Software resilience patterns borrow all three ideas.

2. Kinds of failure

Failure typeWhat happensInstagram exampleWhy it's tricky
Crash (fail-stop)Process or machine dies; connections are refused or reset.A Feed Service pod is OOM-killed.Easiest kind: fast, obvious errors. The load balancer removes it via health checks.
Slow / gray failureStill "up" and passing health checks, but responses take 5 s instead of 20 ms, or 5% of requests fail.A Cassandra node with a degrading disk; a Redis shard swapping memory.Hardest kind. Callers wait instead of failing fast, holding threads and connections. Health checks often say "fine".
Network partitionNodes are alive but can't reach each other (or only some can).A link between two data centers drops; half the Kafka brokers can't see ZooKeeper/controller.From one side, "the other side is dead" and "the network is broken" look identical. Ties into CAP.
Dependency overloadA dependency receives more work than its capacity and its latency explodes (queueing).A celebrity posts; millions of like requests hit one counter shard.Often caused by callers: retries and traffic spikes. Overload spreads.
Bad deploy / bad dataCode or config bug, a poison message that crashes every consumer.A malformed image crashes the media-processing workers in a loop.Affects all replicas at once — redundancy doesn't help. Needs canaries and fast rollback.

A key lesson from industry incident reports: slow is worse than dead. A dead dependency returns an error in milliseconds; you handle it and move on. A slow one silently consumes your resources while you wait.

3. Anatomy of a cascading failure

A cascading failure is when the failure of one component causes the failure of the components that depend on it, and so on, until a large part of the system is down. Walk through a realistic one:

📱 Appsretry on error API Gatewayconnections pile up Feed Serviceall 200 threads busy Rankinghealthy, starved Counter Serviceslow: 3 s per call User Servicehealthy, starved Redis shardhot key, 100% CPU ① origin ② slow ③ thread pool exhausted ④ timeouts → 5xx ⑤ retries ×3 load
One hot Redis shard makes the Counter Service slow; the Feed Service's threads all end up waiting on it, so even requests that need only healthy services fail; client retries multiply the load.
  1. Origin. A celebrity's post goes viral; one Redis shard holding its like counter hits 100% CPU. Its latency rises from 1 ms to 300 ms.
  2. Slowness propagates. The Counter Service calls that shard for every feed that contains the post. Its own latency rises to 3 s. It's not erroring — just slow.
  3. Resource exhaustion. The Feed Service has a pool of, say, 200 worker threads per instance. Normally a feed request takes 100 ms, so each instance handles 2,000 req/s. Now every request waits 3 s on the counter call: capacity drops to 200 / 3 s ≈ 67 req/s — a 30× collapse. Requests queue; queue latency grows; memory grows.
  4. Healthy dependencies starve. Requests that don't even touch the slow counter can't get a thread. Ranking and User Service sit idle while the feed is "down".
  5. Retries amplify. The gateway times out and returns 5xx; apps retry; the gateway may retry too. Load triples exactly when capacity has collapsed.
  6. Death spiral. Overloaded Feed instances fail health checks and get removed, concentrating traffic on the remaining ones, which then fail too. Autoscaling adds instances that immediately receive the backlog and also fall over.

Every pattern in the rest of this lesson breaks one link in this chain: timeouts limit how long you wait (step 3), bulkheads stop one dependency consuming all threads (step 4), circuit breakers stop calling a sick dependency, retry budgets stop amplification (step 5), load shedding keeps an overloaded service doing useful work (step 6), and fallbacks let the feed render without counts.

4. Timeouts

A timeout is the maximum time a caller will wait for a response before giving up. Without one, a single hung dependency can hold a thread forever. Many HTTP clients and database drivers default to no timeout or to very long ones (60 s+) — one of the most common root causes of cascading failures.

4.1 Kinds of timeout

4.2 Choosing values

Too long and you get the cascade above; too short and you fail requests that would have succeeded (and trigger retries). A practical method:

  1. Measure the dependency's latency distribution (see percentiles). Suppose the Counter Service has p50 = 5 ms, p99 = 40 ms, p99.9 = 90 ms.
  2. Set the timeout a bit above a high percentile — e.g. ~100–150 ms. You accept that ~0.1% of calls time out (and maybe get one retry), in exchange for never waiting seconds.
  3. Check it fits in the caller's own budget. If the feed endpoint has a 500 ms SLO and calls 3 dependencies sequentially, their timeouts plus retries must sum to less than that.
  4. Make timeouts configurable at runtime, and alert on timeout rates.

4.3 Deadline propagation

Per-hop timeouts alone waste work. Imagine the app gives up after 2 s, but deeper services each have a 1.5 s timeout and work sequentially — they keep computing an answer nobody is waiting for. Deadline propagation fixes this: the first service sets an absolute deadline ("must finish by 10:00:00.500") and passes the remaining time downstream in a header or RPC metadata. gRPC does this natively (grpc-timeout).

Gateway  deadline = now + 800 ms
  └─► Feed Service           receives "remaining 790 ms"
        ├─► Feed cache (Redis)    gets min(own 50 ms, remaining) → 50 ms
        ├─► Ranking               remaining now 720 ms → its own cap 300 ms
        │     └─► Feature store   remaining 280 ms → cap 40 ms
        └─► Counter Service       remaining 380 ms → cap 100 ms
If remaining ≤ 0 at any hop → don't even start; return DEADLINE_EXCEEDED immediately.
def call_with_deadline(stub, request, ctx, own_timeout_s):
    remaining = ctx.deadline - time.monotonic()
    if remaining <= 0.005:                       # less than 5 ms left: pointless to try
        raise DeadlineExceeded()
    timeout = min(own_timeout_s, remaining)
    return stub.Call(request, timeout=timeout)   # gRPC forwards the deadline downstream

5. Retries with exponential backoff and jitter

Many failures are transient: a packet was dropped, a pod was restarting, a leader election took 2 seconds. Retrying often succeeds. But retries done naively are one of the main causes of outages, so do them carefully.

5.1 What to retry

Retry ✅Don't retry ❌
Connection refused/reset, timeouts on idempotent operations, HTTP 502/503/504, gRPC UNAVAILABLE, 429 after the Retry-After delay.400 Bad Request, 401/403, 404, validation errors (will fail again); non-idempotent writes without an idempotency key; DEADLINE_EXCEEDED when the overall deadline has passed.

5.2 Exponential backoff + jitter

Backoff = wait between attempts, growing exponentially (100 ms, 200 ms, 400 ms…) so a struggling dependency gets breathing room. Jitter = randomise the wait. Without jitter, 10,000 clients that failed at the same instant all retry at exactly +100 ms, then +200 ms — synchronised waves that knock the recovering service over again (a thundering herd). "Full jitter" picks a random wait between 0 and the exponential cap, spreading retries evenly.

import random, time

def call_with_retries(op, max_attempts=3, base=0.1, cap=2.0, deadline=None):
    for attempt in range(max_attempts):
        try:
            return op()
        except RetryableError as e:
            if attempt == max_attempts - 1:
                raise
            if not retry_budget.try_acquire():          # see 5.4 — global brake on retries
                raise
            backoff = min(cap, base * (2 ** attempt))    # 0.1, 0.2, 0.4, ... capped
            sleep_s = random.uniform(0, backoff)         # FULL JITTER
            if e.retry_after:                            # server told us when to come back
                sleep_s = max(sleep_s, e.retry_after)
            if deadline and time.monotonic() + sleep_s >= deadline:
                raise                                    # no time left: fail fast
            time.sleep(sleep_s)
No jitter (10k clients):     ████          ████              ████        ← synchronized spikes
                             t+0.1         t+0.2             t+0.4
Full jitter (10k clients):   ▂▃▂▃▂▂▃▂▂▃▂▂▃▂▃▂▂▃▂▂▃▂▂▃▂▂▃▂▂▃▂▂▃▂▂          ← smooth, survivable

5.3 Retry storms

Retries multiply across layers. Suppose the app, the gateway, the Feed Service and the Counter Service client each make up to 3 attempts. If the bottom dependency is failing, one user action becomes:

attempts at the bottom = 3 (app) × 3 (gateway) × 3 (feed) × 3 (counter client) = 81

An 81× load multiplier, applied precisely when the dependency is already failing — a retry storm. It can keep a service down long after the original cause is gone, because recovery capacity is consumed by retries of requests whose users have already given up.

5.4 Retry budgets and other brakes

6. Idempotency: making retries safe

An operation is idempotent if doing it twice has the same effect as doing it once. GET, PUT profile.bio = "hi" and DELETE like(user=7, post=42) are naturally idempotent. POST /comments or INCR like_count are not: a retried comment after a timeout (where the first attempt actually succeeded but the response was lost) creates a duplicate comment.

The standard fix is an idempotency key: the client generates a unique ID (UUID) per logical action and sends it with every attempt; the server records processed keys and returns the stored result for repeats.

POST /v1/media/42/comments
Idempotency-Key: 6f1c2b9e-0d4a-4c11-9a77-3b2e5f8a9d10
{"text": "Beautiful shot!"}

def create_comment(user_id, post_id, text, idem_key):
    # atomic "insert if absent": only the first attempt wins
    existing = idem_store.get(f"{user_id}:{idem_key}")
    if existing:
        return existing                                  # replay the original response
    comment = comments.insert(post_id, user_id, text)
    idem_store.set(f"{user_id}:{idem_key}", comment, ttl=24*3600)
    return comment

(In production the check-and-insert must be atomic — e.g. a unique constraint on (user_id, idem_key) or SET NX in Redis — to handle two concurrent attempts.) Other idempotency techniques used across Instagram-style systems:

7. Circuit breaker

If a dependency is clearly broken, continuing to call it wastes time (each call waits for a timeout), wastes resources, and adds load to something trying to recover. A circuit breaker wraps calls to a dependency and tracks failures; when failures exceed a threshold it "trips" and subsequent calls fail immediately (or go straight to a fallback) for a cool-down period, then cautiously tests whether the dependency has recovered.

CLOSEDcalls pass through OPENfail fast / fallback HALF-OPENallow a few probes failure rate ≥ 50% over last 20+ calls (or N consecutive failures / timeouts) cool-down (10 s) expires probes succeed a probe fails
Circuit breaker states. CLOSED = normal; OPEN = stop calling and fail fast; HALF-OPEN = let a few trial requests through to decide whether to close again or reopen.
class CircuitBreaker:
    def __init__(self, failure_threshold=0.5, min_calls=20, open_seconds=10, probes=3):
        self.state, self.window = "CLOSED", RollingWindow(seconds=10)
        self.failure_threshold, self.min_calls = failure_threshold, min_calls
        self.open_seconds, self.probes, self.opened_at, self.probe_ok = open_seconds, probes, 0, 0

    def call(self, fn, fallback):
        if self.state == "OPEN":
            if time.monotonic() - self.opened_at < self.open_seconds:
                return fallback()                          # fail fast: no waiting, no load
            self.state, self.probe_ok = "HALF_OPEN", 0     # cool-down over: test the water
        try:
            result = fn()                                  # fn has its own timeout!
        except Exception:
            self.on_failure()
            return fallback()
        self.on_success()
        return result

    def on_failure(self):
        if self.state == "HALF_OPEN":
            self.trip()                                    # still broken → back to OPEN
            return
        self.window.record(ok=False)
        if self.window.count() >= self.min_calls and self.window.failure_rate() >= self.failure_threshold:
            self.trip()

    def on_success(self):
        if self.state == "HALF_OPEN":
            self.probe_ok += 1
            if self.probe_ok >= self.probes:
                self.state = "CLOSED"; self.window.reset()
        else:
            self.window.record(ok=True)

    def trip(self):
        self.state, self.opened_at = "OPEN", time.monotonic()

Design notes:

8. Bulkheads

A bulkhead isolates resources so that one failing part can only exhaust its own compartment. In the cascade above, the Feed Service shared one pool of 200 threads across all dependencies, so the slow Counter Service starved everything. With bulkheads:

                 Feed Service instance (200 threads total)
 ┌───────────────────┬───────────────────┬───────────────────┬──────────────────┐
 │ Ranking pool: 80  │ Feed cache: 50    │ Counters: 30      │ User svc: 40     │
 │ ✓ healthy         │ ✓ healthy         │ ✗ all 30 stuck    │ ✓ healthy        │
 └───────────────────┴───────────────────┴───────────────────┴──────────────────┘
 Counter calls beyond 30 concurrent are rejected instantly → fallback "hide counts".
 The other 170 threads keep serving feeds.

Bulkheads exist at many levels:

9. Load shedding and admission control

Every server has a maximum throughput. When offered load exceeds it, queues grow without bound; every request waits longer; eventually every request exceeds its client's timeout and the server is doing 100% work for 0% useful output ("congestion collapse"). Load shedding deliberately rejects some requests early (cheaply, with HTTP 503/429) so that the rest are served fast.

9.1 Little's law and concurrency limits

Little's law: L = λ × W — the average number of requests in the system equals arrival rate times time in system. If a Feed instance can process 2,000 req/s at 50 ms each, it naturally has ~100 requests in flight. If in-flight climbs to 400, requests must be queueing (W ≈ 200 ms) — a clear overload signal. Admission control caps in-flight requests (or queue length / queue wait time) and rejects new arrivals above the cap. Adaptive limiters (inspired by TCP congestion control, e.g. Netflix's concurrency-limits library) adjust the cap automatically from observed latency.

9.2 Smarter shedding

10. Backpressure

Backpressure is how a slow consumer tells a fast producer "slow down", instead of silently buffering until memory runs out. Load shedding drops work; backpressure pushes the waiting back upstream to where it can be handled.

💡 Example: media processing When many users upload Reels at once, the Upload Service doesn't call the transcoding workers directly — it publishes "video uploaded" events to Kafka. If transcoders fall behind, lag grows, autoscaling adds workers, and the user sees "processing…" a bit longer. Nothing crashes, and no upload is lost. See Media Processing.

11. Fallbacks and graceful degradation

A fallback is what you return when the real answer isn't available in time. Graceful degradation is the product-level design that makes those fallbacks acceptable: the app keeps doing its most important job with reduced quality, rather than failing completely. The key design exercise is to classify every dependency of a request as critical (no answer without it) or optional (nice to have), and to decide in advance what the optional ones degrade to.

FailureDegraded behaviour (Instagram examples)What the user notices
Ranking service down or slowServe the precomputed timeline from the Redis feed cache in reverse-chronological order, or a lighter ranking model; or the last feed cached on the device.Feed is slightly less relevant. Still works.
Counter service failsRender posts without like/view counts (or with last cached counts).Counts missing for a while.
Media upload backend overloadedApp keeps the photo/video locally and queues the upload, retrying in the background with backoff; the post shows "Posting…" .Post appears a bit later.
Stories tray service failsHide the stories tray; feed still loads.No stories row temporarily.
Search index (Elasticsearch) degradedReturn results from a smaller cached "top accounts" index or recent searches.Fewer / less fresh search results.
Recommendation (Explore) unavailableServe a cached generic popular grid per region.Less personalised Explore.
Notification service backlogDelay or drop low-priority pushes (e.g. "X liked your photo" aggregated later); keep DMs priority.Late notifications.
User DB primary unavailableServe reads from replicas; temporarily disable profile edits ("read-only mode").Can browse, can't edit profile.

Rules for good fallbacks:

12. Health checks: liveness vs readiness

Orchestrators (Kubernetes, or Meta's internal equivalents) and load balancers decide what to do with an instance based on health checks. There are two different questions, and mixing them up causes outages:

LivenessReadiness
Question"Is this process fundamentally broken (deadlocked, wedged)?""Should this instance receive traffic right now?"
Action on failureRestart the process/container.Remove from load balancer rotation (don't restart).
What to checkShallow: the process can respond to /healthz; event loop not stuck.Warmed up (caches loaded, config fetched), not overloaded, not draining for shutdown, critical local resources OK.
Must NOT checkDownstream dependencies — if the DB is down, restarting every app server won't fix it and causes a restart storm.Shared dependencies naively — if all instances depend on the same DB and mark themselves unready together, the LB has zero backends: a total outage instead of a partial one.

13. Hedged requests: fighting tail latency

When a request fans out to many servers, the slowest one determines the response time. If each of 100 shards answers in 10 ms 99% of the time but takes 1 s 1% of the time (GC pause, noisy neighbour, disk hiccup):

P(at least one of 100 shards is slow) = 1 − 0.99^100 ≈ 63%
→ the "rare" 1% tail becomes the COMMON case for the fan-out request.

A hedged request sends the request to one replica, and if no answer arrives within, say, the p95 latency, sends a second copy to another replica; use whichever answers first and cancel the other. Because only ~5% of requests trigger a hedge, extra load is ~5%, but the tail shrinks dramatically. (This technique was popularised by Google's "The Tail at Scale" paper.)

t=0ms     send to replica A ─────────────────────────────── (A stuck in GC… answers at 900 ms)
t=12ms    no answer by p95 → send to replica B ──► B answers at t=20 ms ✔ use it, cancel A

14. Chaos engineering

You can't know a system is resilient until you've seen it survive failure. Chaos engineering is the practice of deliberately injecting failures in a controlled way to find weaknesses before real incidents do.

  1. Define steady state: measurable normal behaviour, e.g. feed load success rate > 99.9%, p99 < 800 ms.
  2. Hypothesise: "If one Redis feed-cache shard dies, feed success rate stays > 99.9% because we fall back to the chronological path."
  3. Inject the failure: kill instances, add 500 ms latency to a dependency, drop packets between two services, fill a disk, fail a whole availability zone.
  4. Limit blast radius: start in staging, then 1% of production traffic or one cell; have an abort button.
  5. Observe and fix: if the hypothesis fails, you've found a bug cheaply. Automate the experiment so it runs continuously.

Famous examples: Netflix's Chaos Monkey randomly terminates production instances, and Chaos Kong simulates losing a whole AWS region. Meta has publicly described a program of large-scale disaster-readiness drills (called Storm in public talks) that take entire data centers offline to verify that services — including Instagram — fail over correctly; see Multi-Region. Smaller-scale versions are game days: a team schedules an afternoon to break its own service and practise the response.

15. Patterns at a glance

PatternProblem it solvesInstagram example
TimeoutWaiting forever on a hung/slow dependencyFeed → counter call capped at 100 ms
Deadline propagationDownstream work continuing after the caller gave upGateway's 800 ms budget passed down to ranking and feature store
Retry + backoff + jitterTransient errors; synchronised retry wavesApp re-sends a failed feed fetch after a random 0–400 ms
Retry budgetRetry storms amplifying overload≤ 10% extra calls per client
Idempotency keyDuplicates caused by retriesComment/upload retried without double posting
Circuit breakerRepeatedly calling a broken dependencyStop calling a sick Redis shard for 10 s; hide counts
BulkheadOne dependency exhausting shared resourcesSeparate thread pools per dependency in Feed Service
Load shedding / admission controlCongestion collapse under overloadDrop prefetch requests first at the gateway
BackpressureFast producers overwhelming slow consumersKafka lag for fan-out and transcoding workers
Fallback / graceful degradationTotal failure when an optional dependency failsChronological feed when ranking is down
Liveness / readiness checksRouting to broken or not-yet-ready instancesNew pod receives traffic only after loading its model
Hedged requestsTail latency in fan-outSearch queries to 100 index shards
Chaos engineeringUnknown weaknesses; untested fallbacksData-center drills; killing cache shards in tests

16. How Instagram uses resilience patterns

📱 Apptimeouts, jittered retries, offline queue
→
LB / Gatewayrate limits, shedding, outlier ejection
→deadline
Feed Servicebulkheads, breakers, fallbacks
→
Redis / Cassandrareplicas, hedged reads

Follow one feed load through the main diagram with resilience in mind:

  1. The app requests the feed with a timeout of a few seconds. If it fails, it shows the feed cached on the device and retries with jittered backoff. Uploads and likes performed offline are kept in a local queue with client-generated idempotency keys and replayed when connectivity returns (see Clients).
  2. The load balancer and API gateway enforce rate limits, eject outlier backends, and shed low-priority traffic (prefetch, logging) first under overload. They attach a deadline to the request.
  3. The Feed Service calls its dependencies in parallel through per-dependency bulkheads and circuit breakers. Critical: the timeline from the feed cache (with a fallback to a slower rebuild from the post store). Optional: ranking (fallback: chronological), counters (fallback: hide), stories tray (fallback: hide), ads (fallback: none).
  4. Stores are replicated; reads can go to replicas, and latency-sensitive fan-out reads can be hedged.
  5. Async paths (fan-out, notifications, media processing) run through Kafka with backpressure via consumer lag, idempotent consumers and dead-letter queues for poison messages.
📸 Publicly described practices Instagram's engineering talks describe a Python/Django monolith (the "Django service") running on a very large fleet, where safe, gradual rollouts and the ability to turn features off quickly are core to reliability — code is deployed continuously through canary stages, and feature gates let engineers disable a misbehaving feature without a deploy. Facebook's publicly published "Fail at Scale" article describes the same family of techniques used across Meta: adaptive LIFO queues and controlled-delay queue timeouts for overload, concurrency limits, and regular drills. Exact thresholds and fallbacks are internal; the examples in this lesson are reasonable assumptions about how such a system degrades.

Rough numbers (assumptions)

Feed Service instance: 200 threads, normal latency 100 ms → capacity ≈ 2,000 req/s
If one dependency becomes 3 s slow and there is NO timeout / bulkhead:
    capacity → 200 / 3 s ≈ 67 req/s   (≈ 30× collapse)
With a 100 ms timeout + 30-thread bulkhead on that dependency:
    worst case each request waits 100 ms extra → capacity ≈ 200 / 0.2 s = 1,000 req/s
    and with an OPEN breaker (fail fast) → back to ≈ 2,000 req/s, counts hidden

17. Trade-offs & common pitfalls

✅ Benefits

  • Partial failures stay partial; users get a degraded but working app
  • Fast failure frees resources and shortens incidents
  • Systems recover on their own once the root cause clears
  • Predictable behaviour under overload instead of collapse
  • Confidence from tested failure modes (chaos, drills)

⚠️ Costs

  • More code paths (fallbacks) to build, test and maintain
  • Many tunables (timeouts, thresholds, pool sizes) that can be mis-set
  • Degraded modes can hide real problems if not monitored
  • Retries and hedges add load; breakers can flap
  • Stale data from fallbacks (cached feed, old counts)

Common pitfalls

18. Real-world technologies

ToolWhat it providesWhen to use
Resilience4j (Java), Polly (.NET), failsafe-go, tenacity / backoff (Python)Library-level retries, circuit breakers, bulkheads, rate limiters, timeoutsAdding patterns inside a service's code.
Netflix HystrixThe original popular circuit-breaker/bulkhead library (now in maintenance mode)Historic reference; prefer Resilience4j today.
Envoy, Istio, Linkerd (service mesh)Timeouts, retries with budgets, outlier detection, circuit breaking, load balancing — in a sidecar, language-agnosticMany services in many languages; consistent policy without code changes.
gRPCDeadlines and cancellation propagation, retry policies, health checking protocolInternal RPC between services.
Kubernetes probesLiveness, readiness, startup probes; graceful terminationContainer orchestration.
Netflix concurrency-limits, Envoy adaptive concurrencyAdaptive admission controlAutomatic load shedding based on latency.
Chaos Monkey, Gremlin, LitmusChaos, AWS Fault Injection Service, ToxiproxyFault injection: kill instances, add latency, drop packetsChaos experiments, game days, integration tests of fallbacks.

19. Interview corner

The like-count service is slow and the whole feed is timing out. Walk me through what's happening and how you'd design to prevent it.

Classic cascading failure: Feed Service threads block on slow counter calls, exhausting the shared pool so even requests that don't need counters stall; timeouts at the gateway cause client retries that multiply load. Prevention: (1) a tight timeout on the counter call derived from its p99.9 (~100 ms) and the propagated deadline; (2) a bulkhead — a bounded pool/semaphore for counter calls so it can't consume more than ~15% of threads; (3) a circuit breaker per counter shard that opens on high failure/timeout rates; (4) a fallback: render posts without counts, since counts are optional; (5) retries only at one layer, with a retry budget; (6) load shedding at the gateway; (7) fix the root cause — hot key on the counter shard: shard the counter, cache it, or use approximate counting (see Likes & Counters).

Why add jitter to exponential backoff? Show the formula.

Without jitter, clients that failed together retry together (at +100, +200, +400 ms), creating synchronised spikes that re-overload the recovering service. Full jitter: sleep = random(0, min(cap, base × 2^attempt)). It spreads retries uniformly over the window so the arrival rate is smooth. Also cap attempts, respect the overall deadline and Retry-After, and gate retries with a retry budget.

Explain the three states of a circuit breaker and how you'd tune it.

CLOSED: calls pass; failures are counted in a rolling window. When the failure rate (timeouts + 5xx, not 4xx) exceeds a threshold with a minimum call volume, it goes OPEN: calls fail immediately or use a fallback for a cool-down (e.g. 5–30 s). Then HALF-OPEN: a few probe calls are allowed; if they succeed it closes, if any fails it reopens. Tuning: minimum volume to avoid tripping on noise, failure threshold ~50%, cool-down long enough for a restart, scope per host/shard, alert on transitions, and make sure the fallback is cheap.

A user taps "Post" on a comment, the request times out, and the app retries. How do you avoid a duplicate comment?

The app generates an idempotency key (UUID) when the user taps Post and sends it on every attempt. The server does an atomic insert-if-absent of (user_id, idempotency_key) (unique index or Redis SET NX) in the same transaction as the comment, storing the response; a repeat with the same key returns the original comment instead of inserting again. Keys expire after ~24 h. Downstream, the "comment created" Kafka event carries the comment ID so notification and counter consumers de-duplicate too.

What's the difference between load shedding and backpressure? When would you use each?

Load shedding rejects excess work early (503/429) so the remaining work completes within deadlines — for synchronous, latency-sensitive traffic where waiting is useless (feed loads). Backpressure slows the producer by signalling capacity (bounded queues, pull-based consumers, flow control) so work waits safely upstream — for asynchronous work that must not be lost (media transcoding, fan-out via Kafka lag). Many systems use both: shed at the edge, apply backpressure internally.

Liveness vs readiness — what goes wrong if you get them confused?

Liveness failure → restart; readiness failure → remove from LB. If liveness checks a shared dependency (DB), a DB blip restarts the entire fleet, causing a restart storm and cold caches — turning a brief DB issue into a long outage. If readiness checks a shared dependency, all instances go unready together, leaving the LB with zero backends instead of serving degraded responses. Keep liveness shallow (process responsive), readiness about the instance itself (warm, not overloaded, not draining), and handle dependency failures with breakers and fallbacks instead.

How do hedged requests reduce tail latency, and what are their risks?

With fan-out to N shards, the request is as slow as the slowest shard, so rare per-shard slowness becomes common (1 − 0.99^100 ≈ 63%). Hedging sends a duplicate to another replica if no reply arrives by ~p95, using whichever answers first and cancelling the other — ~5% extra load for a large cut in p99/p99.9. Risks: extra load under overload (so disable or budget it), only safe for idempotent reads, and needs cancellation support to avoid wasted work.

20. Check your understanding

A request depends on 20 services, each 99.95% available, all required. What is its availability?

0.9995^20 ≈ 0.990 → about 99.0%, i.e. ~7 hours of failure per month. This is why you make most dependencies optional with fallbacks: an optional dependency's failure doesn't fail the request.

Four layers each make up to 2 attempts (1 retry). A bottom-layer dependency is down. How many calls reach it per user action, and how would a retry budget change that?

2 × 2 × 2 × 2 = 16 calls. With a 10% retry budget per client, once failures are widespread each layer's budget is exhausted and retries stop, so the amplification approaches ~1.1× per layer (≈1.46× total worst case) instead of 16×. Better still, retry at only one layer.

The Counter Service has p50 = 4 ms, p99 = 35 ms, p99.9 = 80 ms. The feed endpoint has a 400 ms budget. What timeout would you pick and why not 5 seconds?

Around 100 ms: just above p99.9, so ~0.1% of calls time out (and fall back to hidden counts) while fitting comfortably in the 400 ms budget. A 5 s timeout would let a slow counter hold feed threads for 5 s, causing the cascading failure described in section 3; it also exceeds the caller's own deadline, so the work would be wasted.

Your readiness probe checks that PostgreSQL is reachable. The DB primary fails over, taking 20 seconds. What happens?

All instances fail readiness simultaneously; the load balancer removes every backend and the whole service returns errors for 20+ seconds — even reads that could have been served from replicas or caches. Better: readiness reflects the instance's own state; the DB failure is handled by timeouts, breakers and degraded behaviour (e.g. read-only mode).

Which of these operations are safe to retry without an idempotency key: (a) GET feed, (b) POST create comment, (c) PUT set bio to "hi", (d) INCR like counter, (e) DELETE like(user, post)?

(a), (c), (e) are naturally idempotent. (b) creates a duplicate comment on retry; (d) double-counts. Use idempotency keys for (b) and model likes as a set keyed by (user, post) with counts derived, instead of blind increments, for (d).

Under overload a server's queue has requests that have waited 3 s; clients time out at 1 s. Which requests should it serve first, and what should it do with the old ones?

Drop requests that have already exceeded their deadline — the clients have given up, so serving them is pure waste. Serving newest-first (adaptive LIFO) during overload maximises the number of requests completed within their deadlines. Also bound the queue and shed new arrivals early when it's full.

Ranking is down. Give a degraded feed design that keeps the app usable, and one thing you must NOT do as a fallback.

Serve the user's precomputed timeline from Redis in reverse-chronological order (or a light model), optionally merged with the device's cached feed; hide optional modules; log that the response was degraded. You must not bypass critical correctness/security checks — e.g. never skip privacy/block filtering because the service that provides it timed out; fail closed for those.

🎯 Key takeaways

Related lessons