Skip to content

Circuit Breaker

A Circuit Breaker prevents repeated failures when calling an unreliable downstream. It watches call outcomes and, after too many consecutive failures, opens to block further calls for a cool-down period so the dependency can recover.

Why

  • Prevent cascading failures across services.
  • Stop a failing dependency from exhausting every thread or connection in your pool.
  • Expose the health of a dependency at the breaker boundary, so each caller does not have to track it.

State machine

Three normal states (CLOSED, OPEN, HALF_OPEN) plus two manual overrides (FORCED_OPEN, FORCED_CLOSED).

stateDiagram-v2
    direction LR
    [*] --> CLOSED
    CLOSED --> OPEN: failure threshold reached
    OPEN --> HALF_OPEN: after reset_timeout
    HALF_OPEN --> CLOSED: probes succeed
    HALF_OPEN --> OPEN: probe fails
State Description
CLOSED Normal operation. Calls are allowed.
OPEN Calls are blocked to let the dependency recover.
HALF_OPEN A limited number of probe calls test whether the dependency is back.
FORCED_OPEN Manual override that blocks every call.
FORCED_CLOSED Manual override that allows every call.

Usage

from grelmicro.resilience import CircuitBreaker

circuit_breaker = CircuitBreaker(
    "system_name", ignore_exceptions=FileNotFoundError
)


async def async_context_manager():
    async with circuit_breaker:
        print("Calling external service...")


@circuit_breaker
async def async_call():
    print("Calling external service...")


def sync_context_manager():
    with circuit_breaker.from_thread:
        print("Calling external service from AnyIO worker thread...")


@circuit_breaker
def sync_call():
    print("Calling external service from AnyIO worker thread...")

Thread safety

The Circuit Breaker is not thread-safe. Decorated sync functions or from_thread methods ensure state changes run safely within the async event loop. Threaded usage is supported only in AnyIO worker threads and may be slower than pure async usage.

See the API reference for every option.

Configuration

CircuitBreaker follows the three-paths configuration contract.

Programmatic

from grelmicro.resilience import CircuitBreaker

cb = CircuitBreaker(
    "payments",
    error_threshold=5,
    success_threshold=2,
    reset_timeout=30,
)

Declarative

from grelmicro.resilience import CircuitBreaker, CircuitBreakerConfig

config = CircuitBreakerConfig(
    error_threshold=10,
    reset_timeout=60.0,
    ignore_exceptions=(ValueError,),
)
cb = CircuitBreaker.from_config("payments", config)

Environmental

Prefix: GREL_CIRCUIT_BREAKER_{NAME_UPPER}_

Env var Config field Type Default
GREL_CIRCUIT_BREAKER_{NAME_UPPER}_ERROR_THRESHOLD error_threshold int (> 0) 5
GREL_CIRCUIT_BREAKER_{NAME_UPPER}_SUCCESS_THRESHOLD success_threshold int (> 0) 2
GREL_CIRCUIT_BREAKER_{NAME_UPPER}_RESET_TIMEOUT reset_timeout float (> 0) 30.0
GREL_CIRCUIT_BREAKER_{NAME_UPPER}_HALF_OPEN_CAPACITY half_open_capacity int (> 0) 1
GREL_CIRCUIT_BREAKER_{NAME_UPPER}_LOG_LEVEL log_level str "WARNING"
GREL_CIRCUIT_BREAKER_{NAME_UPPER}_IGNORE_EXCEPTIONS ignore_exceptions CSV or JSON list of FQN strings (e.g. builtins.ValueError,my_app.errors.PaymentError or '["builtins.ValueError"]') []

Concrete example for CircuitBreaker("payments"):

GREL_CIRCUIT_BREAKER_PAYMENTS_ERROR_THRESHOLD=10
GREL_CIRCUIT_BREAKER_PAYMENTS_RESET_TIMEOUT=60
from grelmicro.resilience import CircuitBreaker

# GREL_CIRCUIT_BREAKER_PAYMENTS_ERROR_THRESHOLD=10
# GREL_CIRCUIT_BREAKER_PAYMENTS_RESET_TIMEOUT=60
cb = CircuitBreaker("payments")