- At Instagram's scale something is always failing: a crashed host, a slow disk, a flaky network link, an overloaded dependency. Resilience is designing so that partial failure stays partial.
- The worst failures are slow, not dead: a slow dependency ties up threads and connections upstream and causes a cascading failure.
- Core toolkit: timeouts (with deadline propagation), retries with exponential backoff + jitter (within a retry budget), idempotency, circuit breakers, bulkheads, load shedding, backpressure and hedged requests.
- Graceful degradation: when a dependency fails, serve something useful — a cached/chronological feed, hidden like counts, queued uploads — instead of an error page.
- Use liveness vs readiness health checks correctly, and prove it all works with chaos engineering and failure drills.
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.)
2. Kinds of failure
| Failure type | What happens | Instagram example | Why 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 failure | Still "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 partition | Nodes 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 overload | A 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 data | Code 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:
- 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.
- 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.
- 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.
- 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".
- Retries amplify. The gateway times out and returns 5xx; apps retry; the gateway may retry too. Load triples exactly when capacity has collapsed.
- 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
- Connect timeout: how long to wait to establish a TCP/TLS connection. Should be short (e.g. 100–500 ms inside a data center) — if a host doesn't accept connections quickly, it's likely dead.
- Request (read) timeout: how long to wait for the response once connected.
- Overall deadline: the total time budget for an operation including retries.
- Idle timeout: close pooled connections unused for a while (so a silently broken connection isn't reused).
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:
- Measure the dependency's latency distribution (see percentiles). Suppose the Counter Service has p50 = 5 ms, p99 = 40 ms, p99.9 = 90 ms.
- 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.
- 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.
- 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
- Retry at one layer only — usually the layer closest to the failure that knows whether the operation is safe, or the edge (the app). Inner layers fail fast and propagate the error.
- Retry budget: a client may only add retries up to, e.g., 10% of its normal request volume. Implemented as a token bucket: each successful request deposits 0.1 tokens, each retry spends 1. When the dependency is healthy, occasional retries are free; when it's failing broadly, the budget runs dry and retries stop automatically, capping amplification at 1.1×.
- Respect
Retry-Afterand 429/503 from overloaded servers. - Don't retry on overload signals from a server that is explicitly shedding load.
- Combine with a circuit breaker (section 7) so a clearly dead dependency isn't retried at all.
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:
- Natural keys: a like is a row keyed by
(user_id, post_id); inserting it twice is a no-op. Counters are then derived, not blindly incremented. - Client-generated IDs for uploads: the app creates an upload ID before uploading, so a retried upload resumes rather than creating a second post. See also ID generation.
- Consumer de-duplication: Kafka consumers (fan-out, notifications) track processed event IDs because delivery is at-least-once — you don't want two push notifications for one like. See Message Queues.
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.
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:
- Count timeouts and 5xx as failures, but not 4xx (a 404 means the dependency is healthy and answered correctly).
- Use a minimum call volume (here 20) so two failures out of three calls at 3 a.m. don't trip it.
- Scope matters. One breaker per dependency per host or shard is often better than one per whole service: if 1 of 64 Redis shards is sick, you want to stop calling that shard only.
- Limit probes in half-open (only a few requests, not a flood).
- Always pair with a fallback and with metrics/alerts on state changes — an open breaker is a symptom someone should know about.
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:
- Per-dependency thread pools / semaphores / connection pools inside a service (as above).
- Separate service deployments per traffic class: e.g. a separate fleet for internal batch traffic vs user-facing traffic, or for the web vs mobile API, so a scraper storm on one doesn't affect the other.
- Per-tenant / per-customer limits so one abusive client can't use all capacity (see Rate Limiting).
- Cells: whole independent copies of the stack serving a subset of users, the ultimate bulkhead — see Multi-Region & cell-based architecture.
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
- Shed early and cheaply: reject at the load balancer or gateway before doing expensive work (auth, DB reads).
- Priority shedding: drop the least important traffic first. For Instagram: background prefetch of the next feed page and analytics log uploads before user-initiated feed loads; feed loads before… nothing — sending a DM or posting is top priority.
- Queue-time based: if a request has already waited longer than its deadline, drop it — the client has given up. Facebook's publicly described "Fail at Scale" practices include controlled delay (shorten queue timeouts when the queue hasn't been empty recently) and adaptive LIFO (under overload, serve the newest requests first, because the oldest are likely already abandoned).
- Client cooperation: on 429/503 with
Retry-After, apps back off instead of hammering.
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.
- Bounded queues: every in-memory queue has a maximum size. When full, the producer blocks or gets an error. An unbounded queue just converts overload into an out-of-memory crash later.
- Pull-based consumption: Kafka consumers pull at their own pace, so a slow fan-out worker builds up consumer lag (stored durably on disk) rather than being overwhelmed. Lag is then the signal to autoscale workers.
- Flow control in protocols: TCP windows, HTTP/2 and gRPC stream flow control, reactive streams (
request(n)). - Rate limits and 429s upstream: the rate limiter at the gateway is backpressure at the system boundary.
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.
| Failure | Degraded behaviour (Instagram examples) | What the user notices |
|---|---|---|
| Ranking service down or slow | Serve 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 fails | Render posts without like/view counts (or with last cached counts). | Counts missing for a while. |
| Media upload backend overloaded | App 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 fails | Hide the stories tray; feed still loads. | No stories row temporarily. |
| Search index (Elasticsearch) degraded | Return results from a smaller cached "top accounts" index or recent searches. | Fewer / less fresh search results. |
| Recommendation (Explore) unavailable | Serve a cached generic popular grid per region. | Less personalised Explore. |
| Notification service backlog | Delay or drop low-priority pushes (e.g. "X liked your photo" aggregated later); keep DMs priority. | Late notifications. |
| User DB primary unavailable | Serve reads from replicas; temporarily disable profile edits ("read-only mode"). | Can browse, can't edit profile. |
Rules for good fallbacks:
- Fallbacks must be cheaper and more reliable than the primary path — a fallback that calls another fragile service just moves the failure.
- Exercise fallbacks regularly (chaos tests). A fallback path that runs only during real incidents is likely broken when you need it.
- Make degradation visible in metrics and, if needed, to the user ("Couldn't refresh feed. Showing recent posts.").
- Never "fall back" in a way that violates correctness or security — e.g. if the auth/permission check fails, deny (fail closed), don't allow. Showing a private account's posts because the privacy service timed out is a far worse outcome than an error.
- Feature flags / kill switches let operators turn off expensive non-critical features (e.g. disable video autoplay prefetch) during an incident to free capacity.
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:
| Liveness | Readiness | |
|---|---|---|
| Question | "Is this process fundamentally broken (deadlocked, wedged)?" | "Should this instance receive traffic right now?" |
| Action on failure | Restart the process/container. | Remove from load balancer rotation (don't restart). |
| What to check | Shallow: 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 check | Downstream 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. |
- A startup probe gives slow-starting processes (loading an ML model of several GB) time before liveness checks begin.
- Graceful shutdown: on deploy, mark unready first, let the LB drain in-flight requests, then exit.
- Load balancers often add passive health checks (outlier detection): eject a backend that returns many 5xx or is much slower than its peers — this catches gray failures that active
/healthzpings miss. Cap ejection at a percentage (e.g. never eject more than 30% of backends) so a systemic issue doesn't empty the pool. See Load Balancing.
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
- Only hedge idempotent reads (e.g. Search shard queries, feed-cache reads, feature-store lookups).
- Hedge after a delay, not immediately — immediate duplication doubles load.
- Disable hedging under overload (it's a form of retry, so subject it to the retry budget).
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.
- Define steady state: measurable normal behaviour, e.g. feed load success rate > 99.9%, p99 < 800 ms.
- Hypothesise: "If one Redis feed-cache shard dies, feed success rate stays > 99.9% because we fall back to the chronological path."
- 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.
- Limit blast radius: start in staging, then 1% of production traffic or one cell; have an abort button.
- 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
| Pattern | Problem it solves | Instagram example |
|---|---|---|
| Timeout | Waiting forever on a hung/slow dependency | Feed → counter call capped at 100 ms |
| Deadline propagation | Downstream work continuing after the caller gave up | Gateway's 800 ms budget passed down to ranking and feature store |
| Retry + backoff + jitter | Transient errors; synchronised retry waves | App re-sends a failed feed fetch after a random 0–400 ms |
| Retry budget | Retry storms amplifying overload | ≤ 10% extra calls per client |
| Idempotency key | Duplicates caused by retries | Comment/upload retried without double posting |
| Circuit breaker | Repeatedly calling a broken dependency | Stop calling a sick Redis shard for 10 s; hide counts |
| Bulkhead | One dependency exhausting shared resources | Separate thread pools per dependency in Feed Service |
| Load shedding / admission control | Congestion collapse under overload | Drop prefetch requests first at the gateway |
| Backpressure | Fast producers overwhelming slow consumers | Kafka lag for fan-out and transcoding workers |
| Fallback / graceful degradation | Total failure when an optional dependency fails | Chronological feed when ranking is down |
| Liveness / readiness checks | Routing to broken or not-yet-ready instances | New pod receives traffic only after loading its model |
| Hedged requests | Tail latency in fan-out | Search queries to 100 index shards |
| Chaos engineering | Unknown weaknesses; untested fallbacks | Data-center drills; killing cache shards in tests |
16. How Instagram uses resilience patterns
Follow one feed load through the main diagram with resilience in mind:
- 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).
- 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.
- 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).
- Stores are replicated; reads can go to replicas, and latency-sensitive fan-out reads can be hedged.
- Async paths (fan-out, notifications, media processing) run through Kafka with backpressure via consumer lag, idempotent consumers and dead-letter queues for poison messages.
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
- No timeout or library defaults (often infinite or 60 s+).
- Retrying at every layer → multiplicative retry storms; retrying non-idempotent operations → duplicates.
- Backoff without jitter → synchronised retry waves.
- Timeouts longer than the caller's timeout → wasted work after the caller gave up (use deadline propagation).
- Readiness/liveness checks that test shared dependencies → whole-fleet restarts or empty LB pools.
- Unbounded queues → memory blow-up and latency far past any deadline.
- Fallbacks that are never exercised, or that fail open on security checks.
- Autoscaling as the only defence — scaling takes minutes; overload kills in seconds. And a scaled-out caller can overwhelm a dependency that didn't scale.
18. Real-world technologies
| Tool | What it provides | When to use |
|---|---|---|
| Resilience4j (Java), Polly (.NET), failsafe-go, tenacity / backoff (Python) | Library-level retries, circuit breakers, bulkheads, rate limiters, timeouts | Adding patterns inside a service's code. |
| Netflix Hystrix | The 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-agnostic | Many services in many languages; consistent policy without code changes. |
| gRPC | Deadlines and cancellation propagation, retry policies, health checking protocol | Internal RPC between services. |
| Kubernetes probes | Liveness, readiness, startup probes; graceful termination | Container orchestration. |
| Netflix concurrency-limits, Envoy adaptive concurrency | Adaptive admission control | Automatic load shedding based on latency. |
| Chaos Monkey, Gremlin, LitmusChaos, AWS Fault Injection Service, Toxiproxy | Fault injection: kill instances, add latency, drop packets | Chaos 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.
- Partial failure is constant at scale; slow dependencies are more dangerous than dead ones because they exhaust upstream resources and cascade.
- Every remote call needs a timeout, ideally derived from the dependency's p99.9 and a propagated deadline.
- Retry only transient errors on idempotent operations, with exponential backoff + jitter, at one layer, within a retry budget; use idempotency keys for writes.
- Circuit breakers fail fast; bulkheads isolate; load shedding and backpressure keep overloaded systems productive.
- Design graceful degradation per dependency (chronological feed, hidden counts, queued uploads) and never fail open on security.
- Keep liveness shallow and readiness instance-local; prove resilience with chaos experiments and drills.
Related lessons
- Scalability, Availability, Reliability & Performance — the definitions of nines and availability math
- Load Balancing — health checks and outlier ejection
- Rate Limiting — admission control at the edge
- Message Queues & Kafka — backpressure, at-least-once delivery, idempotent consumers
- Microservices — why many dependencies make resilience patterns essential
- News Feed — the request whose fallbacks we designed
- Multi-Region & Disaster Recovery — cells, failover and data-center drills
- Monitoring, Logging & Tracing — seeing timeouts, breaker states and tail latency