Skip to main content

Circuit Breaker / Bulkhead / Retry — Resilience Primitives

Circuit Breaker stops calling a failing dependency. Bulkhead isolates failure domains. Retry handles transient faults.

When to use

  • Any synchronous service-to-service call on a high-traffic path
  • Downstream dependency with variable latency or error rates

Tradeoffs

  • Retry + circuit breaker can amplify load during recovery if not tuned (retry storm)
  • Bulkhead adds configuration and thread/goroutine pool management complexity
type State int

const (Closed State = iota; Open; HalfOpen)

type CircuitBreaker struct {
state State
failures int
threshold int
resetAt time.Time
timeout time.Duration
}

func (cb *CircuitBreaker) Call(fn func() error) error {
if cb.state == Open {
if time.Now().Before(cb.resetAt) {
return errors.New("circuit open")
}
cb.state = HalfOpen
}
err := fn()
if err != nil {
cb.failures++
if cb.failures >= cb.threshold {
cb.state = Open
cb.resetAt = time.Now().Add(cb.timeout)
}
return err
}
cb.state = Closed
cb.failures = 0
return nil
}

Gotcha: Always add jitter to retry delays. Synchronized retries from N clients after a failure = thundering herd. sleep(base * 2^attempt + random(0, base)) is the pattern.