Circuit Breakers and Consistency
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:
- 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.
- 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.
- 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)
/readyzreflecting 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.