principle-resiliency · diff
git:20260822.069aa26 to git:20260828.71a6399
1 added, 1 removed. Audit A to A.
---
name: principle-resiliency
- description: Resiliency principles — fault tolerance, partial failure, blast radius, failure domains, bulkheads, resource isolation, graceful degradation, fail-fast vs fail-soft, health checks, liveness vs readiness probes, cascading and gray failures, idempotency keys, safe retry, deduplication, exactly-once effects, rate limiting, quotas, backpressure, token and leaky buckets, jitter, thundering herds, synchronized retries, and 429 responses. Auto-load when isolating dependencies with bulkheads, planning graceful degradation, choosing fail-fast vs fail-soft, configuring health/readiness/liveness probes, evaluating cascading failures, designing fallback paths, making operations safely retryable with idempotency keys or dedup stores, preventing double execution, designing token-bucket rate limiters, handling 429s and backpressure, or adding jitter to prevent thundering-herd retry spikes.
+ description: Resiliency principles — fault tolerance, partial failure, failure domains, bulkheads, fail-fast vs fail-soft, health checks, liveness vs readiness probes, idempotency keys, safe retry, deduplication, exactly-once effects, token and leaky buckets, synchronized retries, and 429 responses. Auto-load when isolating dependencies with bulkheads, choosing fail-fast vs fail-soft, configuring health/readiness/liveness probes, making operations safely retryable with idempotency keys or dedup stores, preventing double execution, or adding jitter to prevent thundering-herd retry spikes.
---
<!-- preload-canary: SWB-PRELOAD-PRINCIPLE-RESILIENCY -->
# Resiliency
Distributed systems fail partially, not totally. Resilience is the discipline of staying useful when components, networks, or dependencies degrade.
## Failure Domains
A failure domain is the set of components that fail together. Name failure domains before designing for them — unnamed domains produce unnamed blast radii.
- **Crash failure** — process exits; detectable immediately by the load balancer or orchestrator.
- **Slow failure** — process responds but takes too long; the most dangerous mode. Threads and connections fill; the caller eventually crashes too.
- **Gray/Byzantine failure** — process returns wrong data or errors intermittently; hardest to detect.
- **Partial failure** — some instances or shards fail while others serve normally.
Cascading failure: a degraded dependency holds resources long enough that the caller exhausts its own pools, propagating failure upstream. Root cause is almost always unbounded resource sharing across failure domains.
## Bulkheads
Named after ship compartments: isolate resource pools so a breach in one dependency does not exhaust resources for all others.
- Allocate a separate bounded connection pool, semaphore, or thread pool per downstream dependency.
- Never share a single pool across unrelated dependencies — a slow third-party API must not starve database connections.
- In multi-tenant systems, partition queues or workers per tenant to contain noisy-neighbor starvation.
- Size each bulkhead to the dependency's realistic concurrency ceiling, not the caller's maximum.
## Fail-Fast vs Fail-Soft
**Fail-fast** — surface the error immediately without attempting recovery.
**Use when:** an invariant is violated, the operation cannot be retried safely, or the system state is unrecoverable.
**Examples:** config missing at startup, corrupted schema, required auth service unreachable at boot.
**Fail-soft** — continue serving a degraded response using a fallback, stale cache, or reduced feature set.
**Use when:** the failed component is non-critical and a safe, non-misleading fallback exists.
**Examples:** recommendation service down → return empty list; search unavailable → hide the search bar.
Never fail-soft when the degraded response is actively misleading or could corrupt downstream state.
## Survive Faults at I/O Boundaries
Every I/O call must have a deadline. Call-site mechanics (timeouts, retry with backoff, circuit breakers) are covered in `principle-error-handling#Timeouts and Deadlines`, `principle-error-handling#Retries and Backoff`, and `principle-error-handling#Circuit Breakers`. System-level constraints:
- Retries are only safe on idempotent operations — see `principle-api-design#Idempotency`. A non-idempotent retry corrupts state; the retry budget does not excuse skipping idempotency design.
- Circuit breakers and bulkheads are complementary: deploy both for full dependency isolation.
## Graceful Degradation
Define degraded modes before incidents require them. "We'll figure it out" is not a fallback strategy.
- Fallback ladder (cheapest to most expensive to maintain): in-process cache → stale read replica → CDN-cached response → feature-toggle off → friendly error page.
- Shed expensive paths first: drop personalization, search, or aggregations before dropping core reads.
- Read-only mode is a valid degraded state for write-heavy systems when the write path fails.
- Feature toggles allow disabling non-critical paths without a deploy.
## Health Checks
**Liveness** — is the process alive? A failed liveness probe triggers container restart. Only fail liveness on truly unrecoverable conditions (deadlock, corrupted runtime state). Failing liveness during a dependency outage causes restart storms.
**Readiness** — is the process ready to serve traffic? A failed readiness probe removes the instance from rotation without restarting. Fail readiness when a required upstream is unavailable.
**Startup** — is the process still initializing? Prevents liveness from killing a slow-booting instance (cache warm, schema migration).
Shallow check: port open, fast HTTP 200. Always-on.
Deep check: validates dependency connectivity. Readiness only — never liveness.
Cascading-check trap: if every instance polls every dependency on each health interval, a slow dependency produces a check storm that looks like total outage.
For SLI/SLO framing of availability signals, see `principle-observability#SLI / SLO / Error Budget`.
## Idempotency
An operation is idempotent if executing it multiple times produces the same outcome as executing it once. Idempotency is the precondition for safe retry.
- **Idempotency key** — a stable, client-generated ID that uniquely identifies a logical request. The server stores the key; on duplicate submission it returns the stored result instead of re-executing.
- **Safe HTTP methods** — GET, PUT, and DELETE are idempotent by spec. POST is not; add an `Idempotency-Key` request header to make individual POST calls safely retryable.
- **At-least-once vs exactly-once** — networks guarantee at-least-once delivery at best. Exactly-once semantics = at-least-once delivery + idempotent or dedup processing. There is no true exactly-once over an unreliable network without a dedup layer.
- **Dedup store states** — track each key as `pending` (in-flight) or `completed` (result stored). A retry arriving while the original is still pending must wait or return a conflict — never execute a second time.
- **Ordering trap** — reserve the idempotency key (insert `pending`) *before* executing the side effect, not after. Executing first leaves a window where a concurrent retry sees no record and proceeds, causing a double-execute.
For the API contract perspective see `principle-api-design#Idempotency`. For distributed delivery semantics see `principle-distributed-systems#Idempotency Across the Network`.
> See `examples/` for a worked idempotency-key dedup store — prevents double-charge on concurrent retries (read on demand — not auto-loaded).
## Rate Limiting
Rate limiting controls request volume to protect producers from overload and consumers from accidental burst.
- **Token bucket** — bucket holds up to N tokens; each request consumes one; tokens refill at a steady rate. Allows short bursts up to bucket capacity, then enforces the steady-state rate.
- **Leaky bucket** — requests enter a queue and drain at a fixed rate regardless of burst, producing constant-rate output at the cost of higher latency.
- **Fixed/sliding window** — simpler to implement; fixed window has a boundary-burst caveat: a client can send up to 2× the limit across a window boundary. Sliding window tracks per-slot request counts and avoids this burst by weighting across the boundary — at the cost of more state per client.
- **Quota headers** — servers should return `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset`, and `Retry-After`. Clients must honor `Retry-After`; ignoring it escalates to a ban.
- **Client-side backpressure** — use bounded queues that reject new work when full rather than growing unboundedly. A 429 means slow down now, not retry immediately.
- **Jitter on retry** — when many clients hit a rate limit simultaneously, synchronized retries produce a thundering herd at the next interval. Add random jitter; see `principle-error-handling#Retries and Backoff`.
> See `examples/` for a worked token-bucket rate limiter — enforces steady rate, allows controlled bursts, rejects with jitter (read on demand — not auto-loaded).
## When Resiliency Engineering is Overkill
- Single-process tools with no network dependencies; crash-and-restart is the recovery.
- Batch jobs where idempotent restart is sufficient failure handling.
- Prototypes with one developer and no production traffic.
- Internal tools where one team owns both ends of every dependency.
## Red Flags
| Flag | Problem |
|------|---------|
| Single connection pool shared across all dependencies | Slow dependency exhausts connections for every other caller |
| Retry without idempotency | Non-idempotent retries corrupt state; budget wasted |
| Liveness probe that calls a downstream service | Dependency outage triggers container restart storm |
| Deep health check on every load-balancer poll interval | Health-check storm makes partial outage appear total |
| No explicit degraded mode for non-critical paths | Fallback invented under incident pressure |
| Fail-soft returning a misleading or stale result | User confusion or downstream data corruption |
| No per-dependency resource isolation | Any slow caller can exhaust all threads or connections |
| Idempotency key recorded after the side effect | Concurrent retry sees no record → executes the operation a second time |
| Treating exactly-once as achievable without a dedup layer | At-least-once delivery + no dedup = duplicate side effects under retries |
| Rate-limit retry without jitter | Synchronized retries → thundering herd; load spike repeats every retry interval |
| Unbounded client queue under backpressure | Memory grows without limit; OOM instead of graceful rejection |
| Ignoring `Retry-After` header | Client hammers server immediately; risks escalation from throttle to ban |