# PyBreaker: the circuit breaker pattern for Python services

> PyBreaker wraps a failing integration point in a component that stops calling it once it is unhealthy, with configurable failure thresholds, reset timeouts, event listeners, Redis-backed shared state and optional Tornado support. It suits a Python service that talks to a database or queue, and its state is per process unless you give it Redis.

**danielfm/pybreaker** — Python implementation of the Circuit Breaker pattern.

- Repository: https://github.com/danielfm/pybreaker
- Stars: 698 · Forks: 94
- Language: Python
- License: BSD-3-Clause
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/danielfm-pybreaker

## Why the pattern exists: one subsystem failing without taking the rest down

PyBreaker is a Python implementation of the Circuit Breaker pattern, described in Michael T. Nygard's book Release It!.

The readme quotes Nygard directly on the intent: circuit breakers exist to allow one subsystem to fail without destroying the entire system, and this is done by wrapping dangerous operations, typically integration points, with a component that can circumvent calls when the system is not healthy.

That last phrase is the mechanism. The breaker does not make the failing call faster or more reliable. It stops making the call, and it fails fast instead. The value is in what the rest of the system does with that fast failure: a request that hits a dead database gets an error immediately rather than occupying a worker thread, a connection pool slot and a request timeout while the database is down.

The feature list covers the parts of the pattern that are usually fiddly: a configurable list of excluded exceptions such as business exceptions, a configurable failure threshold and reset timeout, support for several event listeners per breaker, the ability to guard generator functions, functions and properties for easy monitoring and management, thread safety, optional Redis backing, and optional support for asynchronous Tornado calls.

The audience is a Python developer protecting an integration point. The requirement is Python 3.10 or later, and the pyproject requires 3.9 or above, so the two are not fully in step and the readme's floor is the one to trust.

## The three ways to call through a breaker, and what the defaults do

The readme shows three equivalent ways to route a call through a breaker, which is a good sign for a library that has to fit into whatever style the host codebase uses.

The decorator form wraps the function:

```python
    @db_breaker
    def update_customer(cust):
        # Do stuff here...
        pass

    # Will trigger the circuit breaker
    updated_customer = update_customer(my_customer)
```

The call form skips the decorator:

```python
    def update_customer(cust):
        # Do stuff here...
        pass

    # Will trigger the circuit breaker
    updated_customer = db_breaker.call(update_customer, my_customer)
```

And the context manager form wraps a block:

```python
    with db_breaker.calling():
        # Do stuff here...
        pass
```

The state machine around those calls is the part with concrete numbers. According to the default parameters, the breaker automatically opens the circuit after 5 consecutive failures. When the circuit is open, all calls fail immediately, raising CircuitBreakerError, without any attempt to execute the real operation. After 60 seconds, the breaker allows the next call through; if that call succeeds the circuit is closed, and if it fails the circuit opens again until another timeout elapses.

That half-open behaviour is the standard pattern, and the readme also documents the option to change what the caller sees. If you want the original error thrown when the circuit trips, set throw_new_error_on_trip to False:

```python
    pybreaker.CircuitBreaker(..., throw_new_error_on_trip=False)
```

And the success_threshold parameter lets a breaker require more than one good call before it closes, which matters for a service that is only partially recovered.

## Where the instance lives, and why that decides whether it works

The readme's instruction is short and easy to miss: CircuitBreaker instances should live globally inside the application scope, e.g. live across requests.

A circuit breaker that is constructed inside a request handler is a new breaker on every call, and a new breaker has never seen a failure, so it never opens. The instance has to outlive the call for the pattern to mean anything.

The first example shows the intended shape, with a comment marking which integration point it protects:

```python
    # Used in database integration points
    db_breaker = pybreaker.CircuitBreaker(fail_max=5, reset_timeout=60)
```

And the success threshold variant:

```python
    # Require 3 successful requests before closing
    db_breaker = pybreaker.CircuitBreaker(fail_max=5, reset_timeout=60, success_threshold=3)
```

The readme also attaches a warning to the whole approach: integration points to external services, databases and queues among them, are more likely to fail, so make sure to always use timeouts when accessing such services if there is support at the API level.

That is the part teams get wrong. A breaker does not protect against a call that hangs forever, it only counts calls that returned, and a request with no timeout will sit inside the worker until the client gives up. The breaker limits the damage of a fast failure, not of a slow one.

One more configuration note: the excluded exception list. A breaker that counts a validation error or a conflict as a failure will open on a healthy service, because the traffic that is most likely to be a business exception is exactly the traffic that is most likely to arrive in a burst. The readme lists the excluded exceptions as a configurable feature, and it is the first thing to set.

## Shared state with Redis, and the namespace rule that prevents a subtle bug

The Redis backing is the feature that makes PyBreaker usable in a multi-process deployment, and the readme is unusually specific about how to configure it.

The storage is passed in as state_storage, with the initial state given as the first argument:

```python
    import redis

    redis = redis.StrictRedis()
    db_breaker = pybreaker.CircuitBreaker(
        fail_max=5,
        reset_timeout=60,
        state_storage=pybreaker.CircuitRedisStorage(pybreaker.STATE_CLOSED, redis))
```

The instruction not to initialize the Redis connection with decode_responses set to True is a genuine trap. It forces returning ASCII objects from Redis, and in Python 3 and above the library will fail with an AttributeError saying a str object has no attribute decode. The failure is at the point of use rather than at connection time, which is why it is worth knowing in advance.

The readme also notes that you may want to reuse a connection already created in your application, with django_redis given as the example:

```python
    import pybreaker
    from django_redis import get_redis_connection

    db_breaker = pybreaker.CircuitBreaker(
        fail_max=5,
        reset_timeout=60,
        state_storage=pybreaker.CircuitRedisStorage(pybreaker.STATE_CLOSED, get_redis_connection('default')))
```

And then the rule that is easy to miss. If you require multiple independent CircuitBreakers and wish to store their states in Redis, it is essential to assign a unique namespace for each CircuitBreaker instance, achieved by specifying a distinct namespace parameter in the CircuitRedisStorage constructor:

```python
    import pybreaker
    from django_redis import get_redis_connection

    db_breaker = pybreaker.CircuitBreaker(
        fail_max=5,
        reset_timeout=60,
        state_storage=pybreaker.CircuitRedisStorage(pybreaker.STATE_CLOSED, get_redis_connection('default'),namespace='unique_namespace'))
```

Without a namespace, two breakers protecting different integration points share one key, so a dead queue can open the breaker for your database. That is the kind of bug that appears in production and is nearly impossible to see in a single-process test.

## Listeners for state changes, failures and successes, without subclassing the breaker

The event listening section makes a deliberate distinction that keeps the design clean: there is no need to subclass CircuitBreaker if you just want to take action when certain events occur, and in that case it is better to subclass CircuitBreakerListener instead.

A listener implements four hooks, and the readme's example shows two listeners with different jobs:

```python
    class DBListener(pybreaker.CircuitBreakerListener):
        "Listener used by circuit breakers that execute database operations."

        def before_call(self, cb, func, *args, **kwargs):
            "Called before the circuit breaker `cb` calls `func`."
            pass

        def state_change(self, cb, old_state, new_state):
            "Called when the circuit breaker `cb` state changes."
            pass

        def failure(self, cb, exc):
            "Called when a function invocation raises a system error."
            pass

        def success(self, cb):
            "Called when a function invocation succeeds."
            pass

    class LogListener(pybreaker.CircuitBreakerListener):
        "Listener used to log circuit breaker events."

        def state_change(self, cb, old_state, new_state):
            msg = "State Change: CB: {0}, New State: {1}".format(cb.name, new_state)
            logging.info(msg)
```

Listeners are attached at creation time or added later:

```python
    db_breaker = pybreaker.CircuitBreaker(listeners=[DBListener(), LogListener()])

    # ...or later
    db_breaker.add_listeners(OneListener(), AnotherListener())
```

The split is the important part. The breaker itself stays a policy object with a state machine, and observation is a separate concern that several listeners can attach to. That is what makes the pattern usable with a logging listener, a metrics listener and an alerting listener at the same time, which is what the feature list means by support for several event listeners per circuit breaker.

The failure hook is also where the interesting information is, since it receives the exception, and the state_change hook receives both the old and the new state, which is enough to emit a metric per transition rather than per call.

## Package shape, tooling and the state of the project

The distribution is a plain Python package with a src/ layout, built with flit_core, and the pyproject classifies it as Development Status :: 5 - Production/Stable, which is the classifier the project itself asserts.

The version is 1.4.1, and the recent release history shows v1.4.1 from 2025-09-21, a v1.4.1-publish-test-1 tag from the same day, and v1.4.0 from 2025-07-06. The test tag in that list is a small signal about the release process, since it shows a publish rehearsal being tagged alongside the real release.

The optional test extras list pytest, mock, tornado, redis, fakeredis and the type stubs, which is the list of the optional features the test suite exercises. The feature list also mentions guarding generator functions and optional asynchronous Tornado calls, and the readme opens that section with a tornado import example, so the async path is a first-class use case rather than an afterthought.

The developer tooling is unusual in a good way. The pyproject carries a full pyprojectx configuration with aliases for install, format, lint, type-check, fix-ruff, test and clean, and a venv block that installs the test requirements plus the package in editable mode. The type-check alias runs mypy with the project python executable and no incremental cache, and the lint alias runs ruff check src. The mypy settings are strict on untyped definitions, with disallow_untyped_defs enabled, and the sdist configuration excludes the test and tooling directories.

The licence is BSD-3-Clause, the repository is not archived, and the last push was on 2026-09-25.

## Conclusion

PyBreaker fits a Python service whose calls to a database, queue or third-party API can hang or fail in a way that should not propagate, and it does not fit a codebase that has already solved this with a shared gateway or a mesh. Before adopting it, decide where the breaker instances live, because a module-level instance per process means each worker keeps its own state unless you back it with Redis, and set the excluded exception list before you set the failure threshold, since a business exception tripping the breaker is the failure mode you will regret.

## FAQ

### What does PyBreaker do and what pattern does it implement?

It is a Python implementation of the Circuit Breaker pattern, described in Michael T. Nygard's book Release It!, wrapping dangerous operations such as integration points with a component that can circumvent calls when the system is not healthy, so one subsystem can fail without destroying the whole system.

### How do I install PyBreaker?

Run pip install pybreaker to get the latest stable version from PyPI, or clone the repository and install in editable mode with git clone, cd pybreaker, ./pw test and pip install -e ., where the ./ prefix is omitted on Windows. The package requires Python 3.10 or later according to the readme.

### How do I call a function through a PyBreaker circuit breaker?

Three ways are documented: as a decorator on the function, by calling db_breaker.call(update_customer, my_customer), or as a context manager with db_breaker.calling(). The breaker instance should live globally in the application scope so it survives across requests.

### Why does PyBreaker need a Redis namespace for each breaker?

If you require multiple independent CircuitBreakers and store their states in Redis, it is essential to assign a unique namespace to each instance by passing a distinct namespace parameter to the CircuitRedisStorage constructor, otherwise the breakers share state and one failing integration point can open another's circuit.

### What happens when a PyBreaker circuit opens?

With the default parameters the circuit opens after 5 consecutive failures, and while it is open all calls fail immediately by raising CircuitBreakerError without attempting the real operation. After the reset timeout of 60 seconds the next call is let through, and if it succeeds the circuit closes, while a failure reopens it for another timeout.

## Sources

- [danielfm/pybreaker on GitHub](https://github.com/danielfm/pybreaker)
- [Issues](https://github.com/danielfm/pybreaker/issues)
- [License: BSD-3-Clause](https://github.com/danielfm/pybreaker/blob/main/LICENSE)
- [README](https://github.com/danielfm/pybreaker/blob/main/README.md)
- [Releases](https://github.com/danielfm/pybreaker/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/danielfm-pybreaker
