Handbooks / Microservices / Chapter 4
Resilience Patterns
7 pages · ~5 min✓ Reviewed
Builds on Data and Consistency. Next up: API Versioning and Contracts.
Failure Containment Mental Model
1. Why Failures Spread
- Resilience pattern = rule limiting how far one bad dependency hurts its callers
- Slow is worse than down: callers hold threads, sockets, memory while waiting
Cascading failure
- 1Service slows
- 2Caller pools back up
- 3Timeouts + retries
- 4Load multiplies upstream
- 5System collapses
2. Layers, Outer to Inner
- Rate limitshed load
- Timeoutbound wait
- Retrytransient only
- Breakerstop sick calls
- Bulkheadisolate
- Fallbackdegrade
3. Retry or Not
| Fault | Examples | Retry? |
|---|---|---|
| Transient | timeout, 503, connection reset, 429 | yes |
| Permanent | 400, 401, 403, 404 | never |
4. Little's Law
- In flight = arrival rate x latency
- 100 req/s: 50 ms is 5 in flight, 5 s is 500
- 100x more threads needed, so the pool is exhausted
Interview Point
Why is slow worse than down? Callers wait holding resources. Which faults to retry? Transient only, never 4xx.
Timeouts and Retries with Backoff and Jitter
1. Timeouts and Deadlines
- Every network call needs an explicit timeout
- Connect timeout: short, ~1 s
- Read timeout: dependency p99 x 1.5
- Default of infinite or 30-60 s = classic outage cause
- Pass one deadline down the chain: gRPC deadline,
X-Request-Deadline - Downstream calls never outlive a caller who gave up
2. Backoff and Jitter
| Strategy | Wait | Note |
|---|---|---|
| Backoff | min(cap, base x 2^n) | 100, 200, 400, 800 ms; cap 10 s |
| Full jitter | random(0, min(cap, base x 2^n)) | Best load spread (AWS) |
| Equal jitter | half fixed + half random | Keeps a minimum wait |
python
delay = min(10, 0.1 * 2 ** attempt) time.sleep(random.uniform(0, delay))
full jitter
- Backoff gives the dependency room to recover
- Jitter breaks synchronized retries
Circuit Breakers and Bulkheads
1. Breaker States
50% of last 20 failafter 30 strials passany fails
- Failure-rate and slow-call thresholds
- Minimum calls before judging the window
- Sliding window: count or time based
- Open wait; permitted trial calls
- One breaker per dependency, not global
2. Bulkhead
- Own bounded pool per dependency: thread pool or semaphore
- e.g. 10 concurrent calls to payments API
- Slow dependency exhausts only its own compartment (ship hull)
- Pool full: reject immediately, never queue unboundedly
3. Breaker vs Bulkhead
| Breaker | Bulkhead | |
|---|---|---|
| Trigger | observed failure | concurrency cap, up front |
| Limits | calls, over time |
Rate Limiting and Load Shedding
1. Limit Per Client
- Caps requests per client per window
- Over limit:
429 Too Many Requests+Retry-After - Rate limit protects the server; breaker protects the client
2. Algorithms
| Algorithm | How | Trait |
|---|---|---|
| Token bucket | B tokens, refill R/s, 1 per request | Burst up to B, avg R; Stripe, AWS |
| Leaky bucket | Queue drained at fixed rate | Smooth constant output |
| Fixed window | Counter per window | Simplest; 2x burst at boundary |
| Sliding window | Log or counter | Fixes burst; more memory |
3. Distributed Keys
- Shared state: Redis
INCR+EXPIRE, or atomic Lua script - Key by user, API key or IP: choose deliberately
- Per tenant keys stop noisy neighbours
Fallbacks and Containing Cascading Failures
1. Fallback
- Fallback = degraded but useful answer when call fails or breaker is open
- Forms: cache, default, static content, simpler algorithm, queue for later
| Failure | Fallback |
|---|---|
| Recommendations down | Show popular items |
| Pricing cache stale | Last known price + flag |
| Primary read fails | Fail over to replica |
| Payment write fails | Enqueue, tell user pending |
2. Fail fast, not silent
- Cheap, local, tested
- Never call another fragile dependency
- Emit metric and alert whenever it fires
3. Containment checklist
- Timeout on every call
- Capped retries, backoff, jitter
- Breaker + bulkhead per dependency
- Rate limit and load shedding at edge
- Fallback for non-critical features
Resilience Cheat Sheet
1. Timeouts and retries
- Connect timeout ~1 s; read timeout ~p99 x 1.5
- Downstream timeout shorter than upstream
- Propagate one deadline down the chain
python
import random def delay(n, base=0.1, cap=10): return random.uniform(0, min(cap, base * 2**n))
full jitter: random(0, min(cap, base x 2^n))
- Max 3 attempts, idempotent calls only
- Retry at one layer; budget ~10% of requests
2. Breaker, bulkhead, limits
50% of 20 failwait 30 spassany fail
| Tool | Rule |
|---|---|
| Breaker | one per dependency |
| Bulkhead | bounded pool per dependency |
| Token bucket | bursts plus steady rate |
| Over limit | 429 + Retry-After |
Check yourself
A calls B, B calls C. Each layer makes 3 attempts per request. C is down. How many calls reach C per request to A?
- 9: each of A's 3 attempts makes 3 attempts at B.
- Retry at one layer only, or the multiplication grows.
A breaker opens at 50% failures over 20 calls, minimum 10 calls. The first 3 calls fail. Open or closed?
- Closed: 3 calls is below the minimum volume.
- Without a minimum, one failure (100%) would trip it.
A service gets 100 req/s. Latency rises from 50 ms to 5 s. A pool of 50 threads was fine before. What now?
- In flight = rate x latency: 5 before, 500 now.
- The pool exhausts and callers queue. Slow is worse than down.