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.

Not all exceptions are equal. A TimeoutError talking to your database is a signal that something is wrong with your infrastructure and the circuit breaker should respond. A ValueError raised because a caller passed bad input is a programming or user error — it says nothing about whether the downstream service is healthy, and tripping the circuit in response would be wrong. PyBreaker is designed around this distinction: it separates system errors from business exceptions and only counts the former toward the failure threshold.

What Counts as a Failure?

By default, any exception raised by the guarded function is treated as a system error and increments the failure counter. This is the safe default — if you have not told PyBreaker what to ignore, it assumes every exception might indicate an unhealthy dependency. Two important rules apply:
  1. Exception subclasses are handled correctly. If you exclude IOError, a subclass such as ConnectionResetError is also excluded, because PyBreaker uses issubclass() for the check.
  2. CircuitBreakerError itself does not count as a new failure. When the circuit is already open and PyBreaker raises CircuitBreakerError to reject a call, that rejection is not recorded as an additional failure — it would be circular to penalise the breaker for doing its job.
You can also exclude exceptions using a callable predicate rather than a class. The callable receives the exception instance and should return True to exclude it:
import pybreaker

def ignore_404(exc):
    """Don't trip the breaker on HTTP 404 responses."""
    return isinstance(exc, HTTPError) and exc.status_code == 404

db_breaker = pybreaker.CircuitBreaker(
    fail_max=5,
    reset_timeout=60,
    exclude=[ignore_404],
)

System Errors vs. Business Exceptions

System errors indicate that the downstream service itself is malfunctioning — the connection was refused, the socket timed out, the database driver returned an unexpected internal error. These are exactly the failures the circuit breaker is designed to track. Business exceptions are raised by correctly-functioning code to communicate domain-level outcomes: a record was not found, the user lacks permission, the submitted form is invalid. The downstream service responded correctly; the caller simply needs to handle the outcome. Tripping the circuit breaker on these exceptions would cause the breaker to open when the service is perfectly healthy.

is_system_error()

PyBreaker exposes its internal classification logic through the is_system_error() method so you can verify that your exclusion rules are working as intended:
import pybreaker

class ValidationError(Exception):
    pass

db_breaker = pybreaker.CircuitBreaker(
    fail_max=5,
    reset_timeout=60,
    exclude=[ValidationError],
)

# Check if an exception would be counted as a system error
db_breaker.is_system_error(ConnectionError())   # True  — increments counter
db_breaker.is_system_error(ValidationError())   # False — excluded, counter unchanged
When is_system_error() returns False for an exception, PyBreaker still re-raises it — the exception propagates to the caller normally — but the failure counter is not incremented and the circuit is unaffected.

Failure Counter Behavior

PyBreaker maintains two counters internally:
CounterAttributeDescription
Failure counterfail_counterConsecutive system errors in closed state
Success countersuccess_counterConsecutive successful calls in half-open state
In the closed state:
  • Each system error increments fail_counter by 1.
  • Any successful call resets fail_counter to 0. Failures must be consecutive to trip the breaker — a single success wipes the slate clean.
  • When fail_counter >= fail_max, the circuit trips to open and fail_counter is not reset.
In the open state:
  • No calls reach the guarded function, so neither counter changes.
  • fail_counter retains its value from when the circuit tripped.
In the half-open state:
  • A successful trial call increments success_counter by 1.
  • Once success_counter >= success_threshold, the circuit closes and both counters reset to 0.
  • A failed trial call opens the circuit immediately; success_counter resets to 0 when the open state is entered.
You can read both counters at any time:
print(db_breaker.fail_counter)     # Current consecutive failure count
print(db_breaker.success_counter)  # Current consecutive success count (half-open)

The throw_new_error_on_trip Option

When the circuit trips — i.e., the call that pushed fail_counter to fail_max — PyBreaker needs to decide what exception to raise to the caller. Default behaviour (throw_new_error_on_trip=True): PyBreaker raises CircuitBreakerError instead of the original exception. This makes it easy to distinguish “the circuit just opened” from subsequent open-circuit rejections and from real downstream errors. Alternative behaviour (throw_new_error_on_trip=False): PyBreaker re-raises the original exception — the one actually thrown by the guarded function — when the circuit trips. Subsequent calls while the circuit is open still raise CircuitBreakerError.
import pybreaker

db_breaker = pybreaker.CircuitBreaker(
    fail_max=5,
    reset_timeout=60,
    throw_new_error_on_trip=False  # Re-raise original exception on trip
)

try:
    db_breaker.call(query_db)
except pybreaker.CircuitBreakerError:
    # Circuit was already open before this call
    return fallback_response()
except ConnectionError:
    # The call that tripped the circuit — original exception propagated
    log.error("Database connection failed and circuit has now opened")
    return fallback_response()
The same behaviour applies in the half-open state: if a trial call fails and throw_new_error_on_trip=False, the original exception is re-raised rather than CircuitBreakerError.

Excluding Exceptions

Configuring which exceptions are treated as system errors — and which are silently passed through without affecting the counter — is one of the most important tuning decisions when deploying PyBreaker. For a full guide including class-based exclusions, callable predicates, and dynamic runtime changes, see the Excluding Exceptions guide.

Build docs developers (and LLMs) love