mrkeyoor.com_
Fri 07 Aug 22:52 UTC
PyPIUtilsupdated 07 Aug 2026

pybreaker

pybreaker implements the circuit breaker pattern from Michael Nygard's Release It!. You wrap a call that talks to something outside your process, such as a database, a queue or a third-party API, and the breaker counts consecutive failures. After fail_max failures it opens: every further call raises CircuitBreakerError immediately instead of waiting on a service you already know is down. After reset_timeout seconds it half-opens and lets one call through to test the water, closing again on success. It is one module of thread-safe pure Python, with optional Redis-backed state so several processes can share one breaker.

Verdict

A clean, small implementation of a pattern most services eventually need, and the Redis storage is what lifts it above a weekend script. The gap that matters is asyncio: on an async codebase you are picking between an awkward fit and a different library.

API stability4/5CircuitBreaker, its constructor arguments and the listener interface have been stable across the 1.x line; the code still carries Python 3.10 compatibility shims and options such as throw_new_error_on_trip and success_threshold were added without breaking older calls
Docs3/5The README is the entire documentation, but it is a good one: every feature has a runnable example and the Redis pitfalls around decode_responses and namespaces are called out explicitly; there is no hosted API reference and no changelog outside the GitHub releases page
Maintenance3/5The repo was pushed 2026-07-04 so the maintainer is present, yet 1.4.1 from 2025-09-21 is still the newest release, and 16 open issues sit on a 689-star single-maintainer project
Ecosystem3/58.4M weekly downloads against 689 stars, which reads as a transitive dependency rather than an active community; no plugin surface, and the only integrations shipped are Redis storage and Tornado

Use it if

  • A slow or dead dependency is dragging your whole service down because every request still waits for its timeout before failing
  • You want failing fast to be a property of the integration point rather than something each call site remembers to implement
  • You need to share breaker state across processes or machines, which the optional Redis storage covers
  • You want hooks on state changes so you can log or alert the moment a dependency starts failing, via CircuitBreakerListener
Skip it if

Setup reality

pip install pybreaker is one small pure Python wheel with no required dependencies, and the API is small enough to learn in one sitting. The real work is placement. Breaker instances have to be module-level or otherwise long lived, because one created per request counts to five failures and then gets garbage collected, which quietly does nothing. Redis backing has two documented traps: initialise the client without decode_responses=True or state reads fail with an AttributeError about str objects, and give every breaker its own namespace or two independent integration points will share one circuit. Note also that the README states Python 3.10 or later while the package metadata still declares 3.9, so do not treat 3.9 as tested.

Patterns

Create one breaker per integration pointcreate-breaker

import pybreaker

db_breaker = pybreaker.CircuitBreaker(
    fail_max=5,
    reset_timeout=60,
    name='customer-db',
)

Module-level, not per request: a short-lived breaker never accumulates enough failures to open.

Guard a function with the decoratordecorator

@db_breaker
def update_customer(cust):
    ...

updated = update_customer(my_customer)

The decorator only handles regular callables and Tornado coroutines; an async def function is not covered.

Wrap a call without decoratingcall-explicit

updated = db_breaker.call(update_customer, my_customer)

The form to use when the function is someone else's, for example a client method you cannot decorate.

Guard a block of codecontext-manager

with db_breaker.calling():
    conn.execute(sql)
    conn.commit()

Anything raising inside the block counts as one failure, so keep the block to a single integration point.

React when the circuit is openhandle-open

import pybreaker

try:
    profile = fetch_profile(user_id)
except pybreaker.CircuitBreakerError:
    profile = cached_profile(user_id)

CircuitBreakerError is raised without calling the wrapped function at all, which is the whole point; this is where a fallback belongs.

Keep raising the underlying errororiginal-exception

db_breaker = pybreaker.CircuitBreaker(
    fail_max=5,
    reset_timeout=60,
    throw_new_error_on_trip=False,
)

Useful when existing error handling already understands the dependency's own exceptions and you do not want to teach it a new one.

Do not count business errors as failuresexclude-business-errors

db_breaker = pybreaker.CircuitBreaker(
    exclude=[CustomerValidationError],
)

db_breaker.add_excluded_exception(DuplicateOrderError)

Subclasses of an excluded type are excluded too; without this, a burst of user input errors will trip a perfectly healthy circuit.

Decide per exception instanceexclude-predicate

db_breaker = pybreaker.CircuitBreaker(
    exclude=[lambda e: isinstance(e, HTTPError) and e.status_code < 500],
)

Types and callables can be mixed in the same exclude list, which is how you keep 4xx responses from counting against an API.

Require several successes before closingsuccess-threshold

api_breaker = pybreaker.CircuitBreaker(
    fail_max=5,
    reset_timeout=60,
    success_threshold=3,
)

Stops a half-open circuit from slamming shut on one lucky response while the dependency is still recovering.

Share breaker state across processesredis-storage

import pybreaker
import redis

client = redis.StrictRedis()  # not decode_responses=True

db_breaker = pybreaker.CircuitBreaker(
    fail_max=5,
    reset_timeout=60,
    state_storage=pybreaker.CircuitRedisStorage(
        pybreaker.STATE_CLOSED,
        client,
        namespace='customer-db',
    ),
)

decode_responses=True fails with an AttributeError about str having no decode; the namespace is required once you have more than one breaker.

Log or alert on state changeslistener

import logging
import pybreaker

class LogListener(pybreaker.CircuitBreakerListener):
    def state_change(self, cb, old_state, new_state):
        logging.warning('breaker %s: %s', cb.name, new_state)

    def failure(self, cb, exc):
        logging.info('breaker %s failure: %r', cb.name, exc)

db_breaker.add_listeners(LogListener())

Listeners run inline on the calling thread, so keep them cheap and never let one raise.

Inspect and drive the breaker at runtimemonitor-state

print(db_breaker.current_state)   # 'open', 'half-open' or 'closed'
print(db_breaker.fail_counter)
print(db_breaker.success_counter)

db_breaker.close()      # force closed
db_breaker.half_open()  # let one call through
db_breaker.open()       # force open

Expose current_state on a health endpoint; forcing open is a useful manual switch during a known upstream incident.

Alternatives

PackageRegistryPick it when
circuitbreakerPyPIYou want a smaller decorator-first breaker and do not need Redis-backed shared state
aiobreakerPyPIYour calls are asyncio coroutines and you want a breaker built around await
tenacityPyPIThe failures are transient, so retrying with backoff fits better than opening a circuit
staminaPyPIYou want retries with sane defaults and jitter, sync or async, rather than breaker semantics