Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/danielfm/pybreaker/llms.txt

Use this file to discover all available pages before exploring further.

PyBreaker models the health of a downstream dependency using three distinct states. Every CircuitBreaker instance is always in exactly one of these states, and each state determines whether an incoming call is executed, rejected immediately, or treated as a probe. Understanding the states — and the conditions that drive transitions between them — is the key to using PyBreaker effectively.

Closed State

Constant: STATE_CLOSED = 'closed' Closed is the normal, healthy operating state. Every call passes through to the guarded function as if the circuit breaker were not there. Behavior:
  • Calls are executed normally.
  • On a successful call, the failure counter resets to 0.
  • On a system error, the failure counter increments by 1.
  • When fail_counter >= fail_max, the circuit trips and transitions to Open.
import pybreaker

db_breaker = pybreaker.CircuitBreaker(fail_max=3, reset_timeout=30)

# Circuit is closed — calls go straight through
result = db_breaker.call(fetch_user, user_id=1)
print(db_breaker.current_state)   # 'closed'
print(db_breaker.fail_counter)    # 0 after a successful call

Open State

Constant: STATE_OPEN = 'open' Open is the tripped state. The circuit breaker has seen too many consecutive failures and is protecting the system by rejecting all calls immediately. Behavior:
  • All calls raise CircuitBreakerError immediately — the guarded function is never invoked.
  • PyBreaker records an opened_at timestamp when the circuit opens.
  • After reset_timeout seconds have elapsed, the next call attempt causes a transition to Half-Open rather than raising immediately.
try:
    result = db_breaker.call(query_db)
except pybreaker.CircuitBreakerError:
    # Circuit is open — fail fast
    return fallback_response()
CircuitBreakerError is a distinct exception class. Catching it separately from other exceptions lets you apply a targeted fallback (cached data, a default value, a user-friendly error message) instead of treating the open-circuit condition the same as a real database error.

Half-Open State

Constant: STATE_HALF_OPEN = 'half-open' Half-open is the trial state. After the reset timeout expires, PyBreaker cautiously allows calls through again to test whether the downstream service has recovered. Behavior:
  • Calls are allowed through to the guarded function as trial calls.
  • On each successful trial call, the success counter increments by 1. Once success_counter >= success_threshold, the circuit closes.
  • On a failed trial call, the circuit immediately opens again and the reset timeout restarts.
  • This prevents your code from hammering a service that is still recovering — traffic only resumes fully once the service has demonstrated it can handle requests successfully.
# After reset_timeout elapses, PyBreaker automatically
# transitions to half-open on the next call attempt.
# No code change needed — the state machine handles it.

try:
    result = db_breaker.call(query_db)
    # If this succeeds, success_counter increments.
    # Once success_counter >= success_threshold, circuit closes.
except pybreaker.CircuitBreakerError:
    # Trial call failed — circuit opened again
    return fallback_response()

State Transitions

The following table describes every valid automatic and manual state transition:
FromToTrigger
CLOSEDOPENfail_counter >= fail_max (threshold reached)
OPENHALF_OPENreset_timeout has elapsed; next call attempted
HALF_OPENCLOSEDsuccess_counter >= success_threshold
HALF_OPENOPENTrial call raises a system error
AnyCLOSEDManual: cb.close()
AnyOPENManual: cb.open()
AnyHALF_OPENManual: cb.half_open()
  ┌─────────────────────────────────────────────┐
  │                                             │
  ▼   fail_counter >= fail_max                  │
CLOSED ──────────────────────────► OPEN         │
  ▲                                  │          │
  │    success_counter >=            │ reset_   │
  │    success_threshold             │ timeout  │
  │                                  ▼          │
  └──────────────────────── HALF-OPEN           │
         trial call fails               ────────┘

State Constants

PyBreaker exports the three state values as module-level string constants:
import pybreaker

pybreaker.STATE_CLOSED    # 'closed'
pybreaker.STATE_OPEN      # 'open'
pybreaker.STATE_HALF_OPEN # 'half-open'
You can compare against these constants when inspecting the current state programmatically:
if db_breaker.current_state == pybreaker.STATE_OPEN:
    log.warning("Database circuit breaker is open — serving from cache")

Manual State Control

You can force a transition to any state at any time using the three control methods:
db_breaker.open()       # Force open (trips the breaker, records opened_at)
db_breaker.half_open()  # Force half-open (allows next trial call)
db_breaker.close()      # Force closed (resets failure and success counters)
Manual state changes bypass the normal failure-counting logic and should be used with care. They are primarily intended for operational intervention (resetting a stuck-open circuit after a confirmed fix) and testing (putting the breaker into a specific state to verify fallback behaviour). Calling close() on a breaker whose downstream dependency is still broken will immediately start re-accumulating failures toward the threshold.

Build docs developers (and LLMs) love