Retries, backoff and circuit breakers0/59 done
Part 3 · System design · Reliability

Retries, backoff and circuit breakers

Retries fix blips. Backoff with jitter stops retries from becoming an attack on your own systems. A circuit breaker stops calling a dependency that is down.

How it works

Retry only what is safe to repeat (idempotent calls) and only errors that might go away: timeouts, 429, 5xx. Wait longer each time, doubling, with random jitter so a thousand clients don't retry at the same instant. Full jitter means wait a random time between 0 and base x 2^attempt, with a cap. A circuit breaker counts failures in a row. Past a threshold it opens and fails fast for a cool-down, which frees your threads and gives the dependency room to recover. Then one trial call goes through (half-open): success closes the breaker, failure opens it again.

closedcalls go through, failures are counted
openfail fast, no calls made
half-openone trial call decides
retry budgetcap retries, for example at 10% extra load

Reach for it when

Implementation (JavaScript, tested)
// Exponential backoff with full jitter: wait random(0, min(cap, base * 2^attempt)).
const backoff = (attempt, { base = 100, cap = 10000, rand = Math.random } = {}) =>
  Math.floor(rand() * Math.min(cap, base * 2 ** attempt));
// After `threshold` failures in a row: fail fast for `coolMs`, then let one trial call through.
class CircuitBreaker {
  constructor({ threshold = 3, coolMs = 5000, now = Date.now } = {}) {
    Object.assign(this, { threshold, coolMs, now, state: "closed", fails: 0, openedAt: 0 });
  }
  call(fn) {
    if (this.state === "open") {
      if (this.now() - this.openedAt < this.coolMs) throw new Error("circuit open"); // protect the callee
      this.state = "half-open"; // cool-down over: one trial
    }
    try {
      const r = fn();
      this.state = "closed";
      this.fails = 0;
      return r;
    } catch (err) {
      if (this.state === "half-open" || ++this.fails >= this.threshold) {
        this.state = "open";
        this.openedAt = this.now();
      }
      throw err;
    }
  }
}

Traps

In real systems: AWS SDKs use exponential backoff with jitter. Resilience4j, Polly and opossum (Node) implement breakers. Envoy and Istio do the same at the network layer with outlier detection.
Say out loud: Timeouts first. Then retries only on idempotent calls, with capped exponential backoff and full jitter. Then a breaker, so a dead dependency fails fast instead of eating every thread.