If you are building distributed systems, network calls will fail. It’s not a matter of if, but when.
Whether it's an API rate limit, a transient database spike, or a downstream service deploying buggy code, external network calls are non-deterministic. The real problem isn't the single failed request—it's cascading failures.
When Service A synchronously calls Service B, and Service B begins hanging due to high latency, Service A runs out of available threads or memory waiting for responses. Suddenly, Service A crashes, taking down Service C, and dragging your entire infrastructure into a full outage.
This is where the Circuit Breaker Pattern becomes vital.
What is a Circuit Breaker?
Inspired by the electrical circuit breaker in your home that cuts power during a current overload, a software circuit breaker wraps an expensive or network-bound call and monitors for failures.
A circuit breaker operates in three distinct states:
+--------------------------------------------+
| |
v |
+-----------+ Failures > Threshold +----------+ |
---| CLOSED |------------------------->| OPEN | | Success
+-----------+ +----------+ |
^ | |
| Timeout Expired | |
| +-------------------+ | |
+------| HALF-OPEN |<---------+ |
+-------------------+ |
| |
+----------------------------+
Failure
- CLOSED (Normal Operation): All requests pass through to the downstream service. The breaker records successes and failures.
- OPEN (Failing Fast): If the failure rate crosses a specified threshold within a time window, the circuit trips OPEN. All incoming requests immediately return a fallback error or cached data without actually making a network call.
- HALF-OPEN (Testing the Waters): After a cooldown period, the breaker allows a limited number of test requests through. If they succeed, the circuit resets to CLOSED. If they fail, it trips back to OPEN.
Implementing a Minimal Circuit Breaker
Here is a lightweight, dependency-free implementation of a Circuit Breaker in Go (the exact same pattern applies in TypeScript, Python, or Java).
go
package main
import (
"errors"
"sync"
"time"
)
type State int
const (
StateClosed State = iota
StateOpen
StateHalfOpen
)
type CircuitBreaker struct {
mu sync.Mutex
state State
failureThreshold int
cooldown time.Duration
failures int
lastFailureTime time.Time
}
func NewCircuitBreaker(threshold int, cooldown time.Duration) *CircuitBreaker {
return &CircuitBreaker{
state: StateClosed,
failureThreshold: threshold,
cooldown: cooldown,
}
}
func (cb *CircuitBreaker) Execute(req func() error) error {
cb.mu.Lock()
// Check if OPEN circuit cooldown period has expired
if cb.state == StateOpen {
if time.Since(cb.lastFailureTime) > cb.cooldown {
cb.state = StateHalfOpen
} else {
cb.mu.Unlock()
return errors.New("circuit breaker is OPEN: fast-failing request")
}
}
cb.mu.Unlock()
// Execute actual network request
err := req()
cb.mu.Lock()
defer cb.mu.Unlock()
if err != nil {
cb.failures++
cb.lastFailureTime = time.Now()
if cb.failures >= cb.failureThreshold {
cb.state = StateOpen
}
return err
}
// Reset state on successful request
if cb.state == StateHalfOpen || cb.state == StateClosed {
cb.failures = 0
cb.state = StateClosed
}
return nil
}
Top comments (0)