-
Notifications
You must be signed in to change notification settings - Fork 0
Circuit Breakers
A circuit breaker stops calling a dependency that is already failing. Without one, every request keeps paying the full timeout to a service that will not answer, and the retries pile on top of whatever is wrong.
cb := health.NewCircuitBreakerWithStore(
health.DefaultCircuitBreakerConfig(),
ext.StateStore(),
"payments-api",
)| State | Behaviour | Leaves when |
|---|---|---|
CircuitClosed |
calls pass through |
FailureThreshold consecutive failures |
CircuitOpen |
calls are refused immediately |
Timeout elapses → half-open |
CircuitHalfOpen |
at most HalfOpenMaxCalls probes |
SuccessThreshold successes → closed; any failure → open |
type CircuitBreakerConfig struct {
FailureThreshold int // 5
SuccessThreshold int // 2
Timeout time.Duration // 30s
HalfOpenMaxCalls int // 1
}DefaultCircuitBreakerConfig() returns those values.
Half-open with HalfOpenMaxCalls: 1 is what stops the thundering herd: when the
timeout expires, exactly one request probes the dependency rather than every
waiting request arriving at once.
if !cb.Allow() {
return nil, &health.CircuitOpenError{DepID: "payments-api"}
}
resp, err := call(ctx)
if err != nil {
cb.Failure()
return nil, err
}
cb.Success()
return resp, nilReset() forces it closed — useful in tests, and for an operator endpoint that
clears a breaker after a fix is deployed.
NewCircuitBreakerWithStore keeps the breaker and the
state store in step: transitions call
SetCircuitState, so the current state appears on
/status and the gate can act on the same
information.
Use it in preference to the bare NewCircuitBreaker whenever a state store
exists — an open breaker that nothing can observe is an outage with no
diagnostic.
The usual way to get one, with no bookkeeping of your own:
client := health.WrapHTTPClient(http.DefaultClient, "payments-api", ext.StateStore(),
health.WithCircuitBreaker(cb),
health.WithHTTPTimeout(3*time.Second),
health.WithRetries(2, 200*time.Millisecond),
)
resp, err := client.Get(ctx, url)
var open *health.CircuitOpenError
if errors.As(err, &open) {
// refused without a call — serve stale, queue, or fail fast
}Every call reports success or failure with its latency, and the breaker refuses calls while open.
FailureThreshold against your traffic rate. Five consecutive failures at
1000 rps is five milliseconds of trouble. On a low-traffic path it may be
minutes. Match it to how long you are willing to keep trying.
Timeout against recovery time. Too short and the breaker reopens
repeatedly while the dependency is still restarting; too long and you stay dark
after it has recovered. 30 seconds is a reasonable default for a service that
restarts in seconds.
SuccessThreshold above 1 for a dependency that can answer one request and
then fail again — a database that accepted a connection from a pool that is
still draining, say.
They overlap and answer different questions:
- The state store's
FailureThresholddecides what/statusand the gate believe about a dependency. - The breaker's
FailureThresholddecides whether the next call is even attempted.
You usually want both, and wiring the breaker to the store with
NewCircuitBreakerWithStore is what keeps their views consistent.
rextension-health — health, readiness and dependency gating for Rex · MIT · © 2026 Kryovyx