Circuit Breaker - Improve Resiliency
A circuit breaker is a client-side resiliency pattern that prevents repeated calls to a failing downstream service.
Instead of endlessly retrying and piling up load, the client fails fast for a short period. This helps:
- protect the downstream system while it recovers
- keep your service responsive (low latency under failure)
- allow you to return a fallback (cached/default response) when appropriate
A circuit breaker is most effective when paired with timeouts and limited retries (with backoff). Github Repo: https://github.com/GolfRider/golang-patterns
States (simple mental model)
A circuit breaker is a small state machine:
- Closed: Everything is normal. Requests go through.
- Open: Too many recent failures. Requests fail fast (no call is made).
- Half-Open: After a cool-down, allow a small “probe” call:
- if the probe succeeds → close the circuit
- if it fails → open again (cool down more)
A Simple Go Implementation
This version tracks consecutive failures. After maxFailures, it opens for resetTimeout.
When the timeout passes, it allows one probe call in Half-Open.
package main
import (
"errors"
"fmt"
"sync"
"time"
)
type CircuitBreakerState int
const (
CBClosed CircuitBreakerState = iota
CBOpen
CBHalfOpen
)
type CircuitBreaker struct {
mx sync.Mutex
lastErrorTS time.Time
resetTimeout time.Duration
errorCount int
maxErrors int
state CircuitBreakerState
}
func NewCircuitBreaker(maxErrors int) *CircuitBreaker {
return &CircuitBreaker{
maxErrors: maxErrors,
resetTimeout: time.Second * 5,
state: CBClosed,
}
}
func (cb *CircuitBreaker) Call(f func() error) error {
// 1. Quick check of state (Acquire lock briefly)
cb.mx.Lock()
if cb.state == CBOpen {
if time.Since(cb.lastErrorTS) > cb.resetTimeout {
cb.state = CBHalfOpen
} else {
cb.mx.Unlock()
return errors.New("circuit breaker is open")
}
}
cb.mx.Unlock()
// 2. Execute the function (OUTSIDE the lock)
err := f()
// 3. Update state based on result (Acquire lock again)
cb.mx.Lock()
defer cb.mx.Unlock()
if err != nil {
cb.errorCount++
cb.lastErrorTS = time.Now()
if cb.errorCount >= cb.maxErrors || cb.state == CBHalfOpen {
cb.state = CBOpen
}
return err
}
// Success logic
cb.state = CBClosed
cb.errorCount = 0
return nil
}
func checkCircuitBreaker() {
cb := NewCircuitBreaker(3)
for i := 0; i < 5; i++ {
err := cb.Call(func() error {
fmt.Println("check-circuit breaker")
return errors.New("circuit breaker is open")
})
if err != nil {
fmt.Println("check-circuit breaker: ", err.Error())
}
}
}