Every checkout depends on services you do not control: payment gateways, fraud scoring, tax calculators, email providers, inventory APIs. When one of them slows down, the naive response is to keep retrying. That is how a single slow dependency turns into a full outage — threads pile up, queues fill, timeouts cascade, and healthy services start failing too.
A circuit breaker stops that spiral. It watches failures, opens when a dependency is clearly unhealthy, and fails fast instead of waiting. The idea comes from electrical breakers, and it maps surprisingly well to distributed systems.
The three states
Closed: Traffic flows normally. The breaker counts failures. A few isolated errors are normal, so it stays closed.
Open: After the failure rate crosses a threshold — for example, 50% failures in a rolling 60-second window, or 20 consecutive failures — the breaker opens. Calls fail immediately without hitting the dependency. This protects your thread pools, your latency budget, and the struggling downstream service, which now gets breathing room to recover.
Half-open: After a cooldown period, the breaker lets a small probe of traffic through. If the probes succeed, it closes again. If they fail, it reopens and waits longer. The half-open state is what makes this a recovery mechanism, not just a kill switch.
Why fail fast matters in checkout
Consider a payment provider that starts responding in 25 seconds instead of 400 milliseconds. Without a breaker, each checkout holds a worker for 25 seconds. Under normal traffic, you exhaust workers in seconds. The checkout page stops loading even for customers whose payment method is fine, because no workers are left.
With a breaker, the payment call fails in milliseconds once the breaker opens. You can then show a clear message — "Payment is temporarily unavailable, please try again in a moment" — and preserve the rest of the experience. Customers can still browse, review their cart, or choose another payment method. That distinction matters commercially: a degraded checkout that explains itself retains far more trust than a hung page. Stores built around instant delivery, such as Kadokey, live or die on that difference, because buyers of digital codes expect confirmation in seconds, not after a timeout loop.
What to return when the breaker is open
Failing fast is only half the job. You need a deliberate fallback for each dependency:
- Payment gateway: Do not silently mark an order as paid. Show the outage, preserve the cart, and let the customer retry without re-entering details.
- Fraud scoring: Fall back to a stricter but safe rule set — for example, require additional verification for high-risk orders — rather than skipping checks entirely or blocking everyone.
- Tax or shipping quotes: Serve a cached estimate with a visible note that the final figure will be confirmed, or defer the quote until the dependency recovers.
- Email delivery: Queue the confirmation and send it when the provider recovers. The order itself should not depend on the email succeeding.
- Non-critical enrichment: Skip it. Recommendation widgets, analytics, and badges should never block a purchase.
The rule is simple: fallbacks must be safe, honest, and reversible. Never invent a success you did not observe.
Tuning thresholds without guessing
Two numbers drive most breakers: the failure threshold and the cooldown.
Start by measuring baseline error rates. If a dependency normally fails 1% of calls, a threshold of 50% failures over 30–60 seconds will catch real outages without tripping on noise. For low-traffic dependencies, use a minimum call volume — say, 20 calls — before evaluating the rate, otherwise two unlucky failures will open the breaker.
Set timeouts separately from the breaker. A call should time out well before the breaker threshold matters. If your timeout is 10 seconds and your breaker allows 20 consecutive failures, you are still holding workers for 200 seconds of failure before protection kicks in. Keep timeouts tight — often 1–3 seconds for synchronous checkout calls — and let the breaker handle the aggregate pattern.
Cooldowns should scale with the dependency. A fast internal service might recover in 10–30 seconds. An external provider under a regional incident may need 60–120 seconds. Some implementations use exponential backoff on repeated failures so the probe rate stays low while the outage persists.
Per-dependency, per-endpoint breakers
One breaker per host is too coarse. A provider can have a healthy refund endpoint and a broken charge endpoint. Give each critical operation its own breaker, and scope breakers by dependency plus operation, not just by service name.
Similarly, do not share a breaker across tenants or regions if failures are isolated. A regional outage should open the breaker for that region only, so other regions keep working. This is the same isolation thinking behind multi-region design: contain the blast radius.
Observability: a breaker you cannot see is a liability
Emit metrics and logs for every state transition: closed to open, open to half-open, half-open to closed, and every rejected call while open. Alert on open breakers, but treat them as symptoms, not the disease. The breaker is doing its job; the underlying dependency is the incident.
Dashboards should show failure rate, rejection rate, and time in each state. That history also helps you tune thresholds after real incidents instead of relying on theory. If you find a breaker opening every day at peak traffic, the threshold is probably wrong — or the dependency needs capacity work, not breaker tuning.
Common mistakes
- Wrapping everything in one breaker. A single global breaker hides which dependency failed and couples unrelated failures.
- No half-open state. A breaker that only opens and never probes requires manual reset, which turns a transient blip into a long outage.
- Treating timeouts as successes. If a call times out, count it as a failure. Otherwise the breaker never sees the slow-dependency case it exists to catch.
- Fallbacks that lie. Returning a fake success, a stale price presented as live, or a skipped fraud check without logging it creates worse problems than the outage.
- No load shedding upstream. Breakers protect downstream services, but you also need concurrency limits and queues so a traffic spike does not overwhelm a recovering dependency the moment the breaker closes.
A minimal mental model
if breaker.is_open():
return fallback()
try:
result = call_dependency(timeout=2s)
breaker.record_success()
return result
except (Timeout, Error):
breaker.record_failure()
return fallback()
Most languages have mature libraries for this — Resilience4j, Polly, Hystrix successors, and similar — so you rarely need to hand-roll the state machine. What you do need to hand-design is the fallback for each call site. That is where the business logic lives.
Putting it together
Circuit breakers are not a replacement for retries, timeouts, or load shedding. They work as a layer: timeouts bound each call, retries handle transient blips with backoff, and breakers stop sustained failures from spreading. Together they let a checkout degrade gracefully instead of collapsing.
Start with your highest-risk dependency — usually payments — add metrics, define an honest fallback, and tune from real data. After one incident where the breaker saves checkout, the value becomes obvious. For a sense of what that resilient checkout should feel like from the buyer's side, look at how streamlined instant-delivery stores present clear status and fast confirmation, for example on Kadokey. The engineering goal is the same: when something fails, the customer should see a clear, fast answer — not a spinning page.
Your users will never thank you for a circuit breaker. They will simply be able to check out while a provider is down — and that is the point.
Top comments (0)