breakr - Circuit Breaker for Go
breakr is a lightweight, production-ready Circuit Breaker implementation for Go.
It helps protect your services from cascading failures and ensures high availability.
📦 Installation
go get github.com/genov8/breakr
Usage
Simple Example
package main
import (
"fmt"
"github.com/genov8/breakr"
"time"
)
func main() {
cb := breakr.New(breakr.Config{
FailureThreshold: 3,
ResetTimeout: 5 * time.Second,
ExecutionTimeout: 2 * time.Second,
})
for i := 0; i < 10; i++ {
result, err := cb.Execute(func() (interface{}, error) {
return nil, fmt.Errorf("error")
})
if err != nil {
fmt.Printf("[Request %d] Circuit Breaker blocked: %v\n", i, err)
} else {
fmt.Printf("[Request %d] Success: %v\n", i, result)
}
time.Sleep(500 * time.Millisecond)
}
}
⚙️ Configuration
| Parameter | Description |
|---|---|
| FailureThreshold | Number of consecutive failures before CB enters Open state. |
| ResetTimeout | Time before CB moves to Half-Open. |
| ExecutionTimeout | Maximum execution time for a protected function. |
| WindowSize | Duration of sliding time window (e.g., 2s). Only failures within this window are counted toward the threshold. Use 0 to disable. |
| FailureCodes | List of HTTP status codes considered failures (e.g., [500, 502, 503]). If omitted, all errors trigger the breaker. |
You can configure breakr using JSON or YAML instead of manual setup.
📝 JSON Example
{
"failure_threshold": 3,
"reset_timeout": "5s",
"execution_timeout": "2s",
"window_size": "10s",
"failure_codes": [500, 502, 503]
}
config, err := config.LoadConfigJSON("config.json")
if err != nil {
log.Fatalf("Error loading config: %v", err)
}
cb := breakr.New(*config)
📝 YAML Example
failure_threshold: 3
reset_timeout: "5s"
execution_timeout: "2s"
window_size: "10s"
failure_codes:
- 500
- 502
- 503
config, err := config.LoadConfigYAML("config.yaml")
if err != nil {
log.Fatalf("Error loading config: %v", err)
}
cb := breakr.New(*config)
🌐 Example 1: Circuit Breaker with HTTP Requests
This example demonstrates how Breakr can handle unstable HTTP requests.
The circuit breaker will trip after 3 failed requests and block further requests until ResetTimeout expires.
package main
import (
"fmt"
"log"
"math/rand"
"net/http"
"time"
"github.com/genov8/breakr"
)
func unstableHandler(w http.ResponseWriter, r *http.Request) {
if rand.Float32() < 0.7 {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
func main() {
// Start an unstable test server
http.HandleFunc("/test", unstableHandler)
go func() {
log.Println("Starting test server on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}()
// Configure Circuit Breaker
cb := breakr.New(breakr.Config{
FailureThreshold: 3,
ResetTimeout: 5 * time.Second,
ExecutionTimeout: 2 * time.Second,
})
// Send requests and observe how CB behaves
for i := 1; i <= 20; i++ {
result, err := cb.Execute(func() (interface{}, error) {
resp, err := http.Get("http://localhost:8080/test")
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= 500 {
return nil, fmt.Errorf("server error: %d", resp.StatusCode)
}
return "Request successful", nil
})
if err != nil {
fmt.Printf("[Request %d] Circuit Breaker blocked: %v\n", i, err)
} else {
fmt.Printf("[Request %d] Success: %v\n", i, result)
}
time.Sleep(500 * time.Millisecond)
}
}
⚡ Example 2: Filtering Specific Failure Codes
This example shows how to configure Breakr to only react to certain HTTP error codes (500, 502, 503) while ignoring others like 404.
package main
import (
"errors"
"fmt"
"time"
"github.com/genov8/breakr"
)
// APIError — custom error type with an HTTP status code
type APIError struct {
StatusCode int
Message string
}
func (e *APIError) Error() string {
return e.Message
}
func (e *APIError) Code() int {
return e.StatusCode
}
func main() {
// Create a Circuit Breaker with FailureCodes support
cb := breakr.New(breakr.Config{
FailureThreshold: 3,
ResetTimeout: 5 * time.Second,
ExecutionTimeout: 2 * time.Second,
FailureCodes: []int{500, 502, 503}, // Reacts only to these error codes
})
// API call returning 500 (critical failure)
fail500 := func() (interface{}, error) {
return nil, &APIError{StatusCode: 500, Message: "Internal Server Error"}
}
// API call returning 404 (ignored error)
fail404 := func() (interface{}, error) {
return nil, &APIError{StatusCode: 404, Message: "Not Found"} // Ignored
}
// Successful API call
success := func() (interface{}, error) {
return "Success", nil
}
// Simulating requests
for i := 1; i <= 5; i++ {
result, err := cb.Execute(fail404)
if err != nil {
fmt.Printf("[Request %d] Ignored Error: %v\n", i, err)
} else {
fmt.Printf("[Request %d] Success: %v\n", i, result)
}
}
for i := 6; i <= 10; i++ {
result, err := cb.Execute(fail500)
if err != nil {
fmt.Printf("[Request %d] Failure: %v\n", i, err)
} else {
fmt.Printf("[Request %d] Success: %v\n", i, result)
}
}
// Circuit Breaker should now be in Open state
for i := 11; i <= 13; i++ {
result, err := cb.Execute(fail500)
if err != nil {
fmt.Printf("[Request %d] Circuit Breaker blocked: %v\n", i, err)
} else {
fmt.Printf("[Request %d] Success: %v\n", i, result)
}
}
// Waiting for ResetTimeout
fmt.Println("⏳ Waiting for ResetTimeout...")
time.Sleep(6 * time.Second)
// Checking Half-Open state, should allow a successful request
result, err := cb.Execute(success)
if err != nil {
fmt.Printf("[Request 14] Failure: %v\n", err)
} else {
fmt.Printf("[Request 14] Success: %v\n", result)
}
// Should fully recover after success
result, err = cb.Execute(success)
if err != nil {
fmt.Printf("[Request 15] Failure: %v\n", err)
} else {
fmt.Printf("[Request 15] Success: %v\n", result)
}
}
🌐 Example 3: Sliding window
This example demonstrates how Breakr uses a sliding time window to only consider recent failures toward the threshold.
package main
import (
"fmt"
"github.com/genov8/breakr"
"github.com/genov8/breakr/config"
"time"
)
type APIError struct {
StatusCode int
Message string
}
func (e *APIError) Error() string {
return e.Message
}
func (e *APIError) Code() int {
return e.StatusCode
}
func main() {
cb := breakr.New(config.Config{
FailureThreshold: 2,
ResetTimeout: 3 * time.Second,
ExecutionTimeout: 1 * time.Second,
WindowSize: 2 * time.Second,
FailureCodes: []int{500},
})
fail := func() (interface{}, error) {
return nil, &APIError{StatusCode: 500, Message: "Internal Server Error"}
}
success := func() (interface{}, error) {
return "OK", nil
}
_ = success
cb.Execute(fail)
fmt.Println("[1] First failure")
time.Sleep(3 * time.Second)
cb.Execute(fail)
fmt.Println("[2] Second failure (alone in window)")
fmt.Printf("[2] State: %s\n", cb.State())
cb.Execute(fail)
fmt.Println("[3] Third failure — should trip breaker")
fmt.Printf("[3] State: %s\n", cb.State())
// Output:
// [1] First failure
// [2] Second failure (alone in window)
// [2] State: Closed
// [3] Third failure — should trip breaker
// [3] State: Open
}
📜 Circuit Breaker States
- Closed → Everything works fine, requests are allowed.
- Open → Requests are blocked after reaching the failure threshold.
- Half-Open → A test request is allowed to check if recovery is possible.
[Closed] → (errors > threshold) → [Open] → (timeout expires) → [Half-Open]
↑ ↓
└── (success) ← (failure) ───────────────────┘
🎯 Key Features
- [x] Protects against cascading failures
- [x] Limits retries to avoid overloading a failing service
- [x] Fast & lightweight
- [x] Supports execution timeouts
- [x] Allows filtering which errors trigger the breaker (
FailureCodes) - [x] JSON & YAML configuration support
- [x] Sliding window strategy — count only recent failures in a time window
Log in or sign up for Devpost to join the conversation.