Skip to main content

Circuit Breakers and Consistency

advanced30 min readLesson 149 of 169

Stop calling dependencies that are already dying, and learn to reason about data that is temporarily wrong.

The circuit breaker

Calling a dead service has a cost: each call burns a timeout, a thread, and user patience. The circuit breaker (borrowed from electrical engineering) tracks recent failures and trips OPEN after a threshold:

  • CLOSED โ€” normal; calls pass through; failures increment the count.
  • OPEN โ€” calls fail instantly (fast-fail) without touching the network; this also gives the dependency room to recover. After a cool-down, allow one probe through.
  • HALF-OPEN โ€” the probe call decides: success closes the circuit, failure re-opens it.
class CircuitBreaker:
    def __init__(self, threshold=5, cooldown=30.0):
        ...
    def allow(self, now): ...      # may we call right now?
    def record(self, now, ok): ... # outcome of a real call

Notice both methods take now โ€” deterministic, testable, injectable time. That's a recurring theme: every resilience mechanism is a state machine whose transitions depend on time, so make time a parameter.

Consistency: the cost of copying

Once data lives on two machines, updates take time to propagate. During that window reads disagree: eventual consistency. Three everyday consequences:

  1. Read-your-writes: after a user updates their profile, the very next read should reflect it โ€” route that read to the primary, or version reads.
  2. Stale reads are a product decision: a slightly-old follower count is fine; a slightly-old account balance is not. Classify data by staleness tolerance.
  3. Optimistic concurrency: instead of locking, write with a version: UPDATE ... WHERE version = :seen โ€” 0 rows updated means someone raced you; re-read and retry. Cheap, and correct under contention.

Distributed locks: necessary, and dangerous

A lock service can grant the same lock twice (GC pause, network partition). So: never let correctness depend solely on a distributed lock โ€” pair it with an idempotency token or fencing token (a number that increases with each grant; older holders get rejected). The lock is an optimization; the fencing is the correctness.

Health checks and graceful shutdown (service hygiene)

  • /readyz reflecting real dependency state lets the load balancer drain a sick instance instead of feeding it traffic.
  • On SIGTERM: stop accepting new work, finish (or re-queue) in-flight jobs, then exit. Kill-9 mid-job is only safe when jobs are idempotent โ€” which is the design you already have.

Now practice

Trip the BreakerA circuit breaker as a deterministic state machine: CLOSED, OPEN at threshold, HALF-OPEN probe after cooldown.1 challenge ยท ยท ~20 minDistributed Job System Mini-ProjectPush a job through an unreliable network to an unreliable queue, with retry budgets and honest failure.1 challenge ยท ยท ~30 min