Reference · gazar.dev ← All cheat sheets

Reliability · resilience

The resilience patterns cheat sheet.

S9 said it: at scale something is always broken, so you contain failure, you don't prevent it. This sheet is the whole toolkit on one page: the four patterns that bound a call, how to degrade instead of dying, how to stay correct under retry, and how to choose a failure mode on purpose. Companion to S9 · Designing for Failure.

01

First move: contain, then choose a failure mode

Before any specific pattern, two questions frame the work. What does this failure take down with it (the blast radius), and when a dependency is gone, what should this path do? Everything else is the tooling that answers those two.

C

Contain the blast

Walk the dependency graph and label each edge hard (caller dies without it) or soft (caller degrades but survives). The blast radius is everything reachable through hard edges. The cheapest win is turning a hard edge soft.

M

Choose the failure mode

For each path, decide in advance: does it fail open (stay available, permissive) or fail closed (refuse, safe)? Saying which, out loud, per path, is the highest-signal sentence in a design review.

The trap

Treating reliability as "make it not fail." At scale that is impossible, and chasing it hides the real question. The grown-up framing is "when this fails, and it will, what stays up and what do we do?" Patterns are how you answer; they are not a substitute for answering.

02

The patterns at a glance

Each pattern bounds one axis or chooses one behaviour. Know which failure each one prevents.

PatternBounds / choosesPreventsOne-line example
Timeouttime per calla hang holding the thread forevercap the DB query at 400ms
Retry + jitterrepetitiona transient blip becoming a failureretry a 503, backoff, spread the herd
Circuit breakerpersistencehammering a dependency that is downfail fast for 30s, then test once
Bulkheadconcurrencyone slow dependency draining all threadsseparate pool per dependency
Fallbackbehavioura blank error when a default would doserve a generic list if recs are down
Graceful degradationbehaviourgoing fully dark under strainread-only mode when the DB is hot
Load sheddingadmissionoverload collapsing the whole servicedrop low-priority traffic at the edge

The one-line map

Four patterns bound a failure: timeout (time), retry (repetition), breaker (persistence), bulkhead (concurrency). Three patterns choose what happens when something is gone: fallback, graceful degradation, load shedding. Reach for a bounding pattern to keep a failure local; reach for a behaviour pattern to keep the user served.

03

The four bounding patterns

Each bounds a different axis of a call. Together they stop a single slow dependency from climbing the stack into a site-wide outage. Each card: bounds what it limits, use how to apply it, watch the catch.

Timeoutbounds time
Bounds
how long any one call may take. A crash is honest; a hang holds the thread, connection and user indefinitely.
Use
give every outbound call a deadline, and make them a budget: each hop gets a slice of the time the hop above it had left.
Watch
infinite or missing timeouts are the default in many clients. A too-tight timeout turns a slow-but-fine call into a failure, tune to p99.
Retry + backoff + jitterbounds repetition
Rule
retry only idempotent, transient failures (timeout, 503), never a 400. Back off exponentially and add jitter.
Use
paper over a brief blip without a thundering herd. Jitter desynchronises clients so they don't all retry on the same beat.
Watch
retrying non-idempotent writes double-charges. Retrying in lockstep re-kills a recovering service (the retry storm).
Circuit breakerbounds persistence
States
closed (calls flow, failures counted) → open (fail fast, no calls) on a high failure rate → half-open (one trial call) after a cool-off.
Use
stop wasting threads on a dependency that is clearly down, and give it room to recover instead of hammering it.
Watch
no half-open means you either stay dark forever or slam it back open blind. Tune the threshold to avoid flapping.
Bulkheadbounds concurrency
Idea
give each dependency its own bounded thread or connection pool, like watertight compartments in a ship.
Use
stop one slow dependency from starving every other path. A stuck recommendations call can't hold the threads checkout needs.
Watch
more pools means more tuning and some idle capacity. Size each pool to its dependency, not one global guess.

Circuit breaker: the state machine

stateDiagram-v2
    CLOSED: CLOSED
calls flow, failures counted OPEN: OPEN
fail fast, no calls HALF_OPEN: HALF-OPEN
one trial call CLOSED --> OPEN: failure rate over threshold OPEN --> HALF_OPEN: cool-off elapsed HALF_OPEN --> CLOSED: trial call succeeds HALF_OPEN --> OPEN: trial fails classDef good fill:#D6F5E3,stroke:#1B7F46,color:#09244B classDef bad fill:#FEE4E2,stroke:#C8102E,color:#09244B class CLOSED good class OPEN bad
04

Fallback, degradation & load shedding

When you can't serve the perfect response, serve a useful one. These are designed modes: decide in advance what you drop first and what you protect last, so the user notices less rather than everything at once.

Fallbacka safe default
Idea
when a dependency is gone, return a sensible default instead of an error: a generic list, a cached value, an empty-but-valid response.
Watch
only for paths where a wrong-but-safe answer beats no answer. Never fall back on a path where correctness is safety-critical.
Graceful degradationserve less, not nothing
Modes
serve stale from cache, drop optional features, go read-only when writes are strained. Partial service beats a blank page.
Watch
degradation has to be designed and tested, or it becomes an accidental cascade. Know which features are sheddable before the incident.
Load sheddingprotect the core
Idea
under overload, reject low-priority traffic early (at the edge) so the core path keeps meeting its SLO instead of everything collapsing together.
Watch
shed by priority, not at random. Pair with backpressure so callers slow down rather than retrying into the wall.

The cascading failure these prevent

One slow dependency fills retry queues, the retries exhaust a shared pool, callers time out and retry harder, and the slowness climbs until the whole site is down, often after the original fault healed. Bounding patterns are the firebreaks; degradation and shedding are how the parts still standing keep serving.

05

Correctness under retry

Retries and at-least-once delivery mean the same operation will arrive twice. The fix is not to prevent duplicates; it is to make duplicates harmless. This is the other half of resilience: staying correct while you retry.

ConceptWhat it isWhen to reach for it
Idempotency keya client-supplied unique key the server dedupes on, so a repeated request applies onceany non-idempotent write that may be retried (payments, orders)
At-least-oncethe realistic delivery guarantee: messages may arrive more than once, never zerodefault for queues; pair with idempotent consumers
Exactly-onceat-least-once delivery plus idempotent processing; true end-to-end exactly-once is mostly a mythwhen you say it, you mean dedupe on the consumer
Dead-letter queuewhere a message goes after N failed retries, so a poison message stops blocking the queueevery consumer, so one bad message can't wedge the pipe

Idempotency key: make "twice" safe

POST /charges
Idempotency-Key: a1b2-c3d4     (client generates one per logical action)

server:
  seen = lookup(a1b2-c3d4)
  if seen:  return seen.response        # duplicate: replay, do NOT charge again
  else:     result = charge_card()
            store(a1b2-c3d4, result)
            return result

=> the timeout-then-retry that would double-charge now applies once.
   at-least-once delivery + idempotent handler = effectively exactly-once.
06

Fail-open vs fail-closed

When a dependency dies, a path either keeps working permissively (fail-open, favours availability) or refuses safely (fail-closed, favours safety). There is no global answer; it is a per-path call, and the trade is security versus availability.

Path / dependency downChoiceWhy
Auth / permission checkfail closedan open turnstile is a security hole; deny access rather than leak it
Fraud / risk scoringfail closedmoney is safety-critical; hold the charge rather than wave it through
Recommendations / "you may also like"fail opena generic list beats blocking the shop; uptime beats perfection
Personalisation / non-critical enrichmentfail openserve the un-personalised version; the core path still works
Rate limiter backing store downit dependsfail open risks abuse; fail closed risks an outage, decide by which hurts more

The decision in one line

Ask: if this check is unavailable, is it safer to let traffic through or to stop it? Security-critical paths fail closed; availability-driven, non-critical paths fail open. The senior move is naming the choice per path, in the design, before the incident decides for you.

07

Operational glue

The supporting practices that make the patterns work in production. Without these, a breaker has nothing to trip on and a blast radius is a guess.

Health checksliveness vs readiness
Rule
liveness = restart me if I'm wedged; readiness = take me out of rotation if I can't serve. Keep them separate.
Watch
a readiness check that calls a shared dependency can take the whole fleet out at once when that dependency blips.
Blast-radius mappingknow before it breaks
Rule
before launch, list each dependency edge as hard or soft and trace what one failure takes down. Soften the worst hard edge.
Watch
hidden transitive dependencies (a shared cache, a common auth service) widen the radius more than the obvious graph shows.
Dependency isolationcontain by design
Rule
isolate critical paths from optional ones: separate pools (bulkhead), separate deploys, async where you can so a sync failure can't block.
Watch
a single shared resource (thread pool, DB connection, mutex) quietly couples everything that touches it.
08

Symptom → pattern

Start from what is hurting and reach for the pattern that bounds or chooses around it.

flowchart TB
    Q{"what is the failure mode?"}
    Q --> A1["a call hangs, no response"]
    A1 --> A2["TIMEOUT, budget per hop"]
    Q --> B1["a dependency blips intermittently"]
    B1 --> B2["RETRY plus backoff plus JITTER"]
    B2 --> B3["and the write is not idempotent"]
    B3 --> B4["plus IDEMPOTENCY KEY"]
    Q --> C1["a dependency is hard-down"]
    C1 --> C2["CIRCUIT BREAKER, fail fast"]
    Q --> D1["one slow dep starves everything"]
    D1 --> D2["BULKHEAD, isolate pools"]
    Q --> E1["a dependency is gone but optional"]
    E1 --> E2["FALLBACK or graceful DEGRADATION"]
    Q --> F1["the service is overloaded"]
    F1 --> F2["LOAD SHEDDING plus backpressure"]
    Q --> G1["the check itself is unavailable"]
    G1 --> G2["choose FAIL-OPEN vs FAIL-CLOSED"]
    classDef accent fill:#DCEBFE,stroke:#4A90D9,color:#09244B
    class A2,B2,B4,C2,D2,E2,F2,G2 accent
You want to…Reach forPairs with
Stop a hang holding a threadTimeout (budgeted)deadlines propagated per hop
Survive a transient blipRetry + backoff + jitteridempotency, circuit breaker
Stop hammering a dead dependencyCircuit breakerfallback for the open state
Stop one dep starving the restBulkheadseparate pools, async
Keep serving when a dep is goneFallback / degradationserve stale, read-only
Make a retried write safeIdempotency keydedup table, at-least-once
Survive overloadLoad sheddingbackpressure, priority
09

Interview questions, with crisp answers

The resilience questions that come up, and the one or two sentences that show you understand the trade-off rather than reciting a term.

Q · Why is a hang worse than a crash?

A crash frees the thread and surfaces an error you can handle. A hang holds the thread, connection and user indefinitely, and a few hangs exhaust the pool and take everything down. Bound every call with a timeout.

Q · Why add jitter to retries?

Without it, every client retries on the same beat and the synchronised spike re-kills a recovering service (a retry storm). Jitter spreads retries over a window so the service can actually come back.

Q · Walk the circuit breaker states.

Closed: calls flow, failures counted. Over threshold it trips open: fail fast, no calls, for a cool-off. Then half-open: one trial call. Succeeds, close; fails, open again. Half-open is what lets it recover safely.

Q · Bulkhead vs circuit breaker?

A breaker stops calling a dependency that is down. A bulkhead isolates resources so one slow dependency can't starve the others. One bounds persistence, the other bounds concurrency; you usually want both.

Q · How do you make a retried payment safe?

An idempotency key per logical charge: the server dedupes on it and replays the stored response instead of charging again. That turns a timeout-then-retry from a double-charge into a no-op.

Q · Is exactly-once delivery real?

End to end, rarely. You take at-least-once delivery and make the consumer idempotent, so duplicates are harmless. That combination is what people mean by exactly-once.

Q · Fail-open or fail-closed for auth?

Closed. If the auth check is unavailable, deny rather than risk leaking access. Recommendations, by contrast, fail open. The choice is per path: security-critical closed, availability-driven open.

Q · What is a dead-letter queue for?

A place to park a message after N failed retries so a single poison message can't block the whole queue. You alert on the DLQ and process or discard it out of band.

10

Traps & the repeatable move

The ways resilience goes wrong, and the habit that catches all of them.

The traps

  • Retry without jitter, synchronised retries become the outage you were retrying around.
  • Infinite or missing timeout, one hang holds the thread and the user forever.
  • Breaker with no half-open, you stay dark, or slam it back open blind.
  • One shared pool, the slowest dependency starves every other path.
  • Retrying non-idempotent writes, the timeout-then-retry double-charges.
  • Fallback on a safety-critical path, a wrong-but-available answer where you needed a correct one.
  • Readiness check on a shared dependency, one blip pulls the whole fleet out of rotation.

The repeatable move

1. what is the blast radius? hard vs soft edges
2. bound every call: time, repetition, persistence, concurrency
3. retried? make it idempotent
4. dependency gone? fail open or closed, on purpose

The tell in a design review: a call with no timeout, a retry with no jitter or idempotency, or a failure mode nobody named. Spot the gap and push back.

Companion to S9 · Designing for Failure. Further reading: Azure · Circuit Breaker pattern · Azure · Bulkhead pattern · AWS · Timeouts, retries and backoff with jitter · Fowler · CircuitBreaker · Google SRE · Handling overload.