pybreaker review
PyBreaker 1.4.1 counts failures around a callable and opens a circuit after the configured threshold, causing later calls to fail immediately with `CircuitBreakerError`. After `reset_timeout`, one half-open trial decides whether recovery has started; `success_threshold` can require several good trials before closing. Decorators, direct calls, and a context manager all feed the same state machine. Listeners report transitions, exclusion rules keep business errors out of the count, and Redis storage can share state between processes. The async API is specifically for Tornado generator coroutines, so ordinary `async def` code is a poor match.
PyBreaker 1.4.1 installed in 0.5 seconds as 1 MB and imported in 0.14 seconds in our sandbox, making it a cheap circuit state machine for synchronous Python services. Install it for shared Redis-backed tripping or listener hooks; use an await-native breaker for asyncio and keep real I/O timeouts either way.
We installed it
| Install | ✓ · 0.5s | 1 package on disk · 1 MB |
| Import | ✓ | import pybreaker in 0.14s · pure Python · py.typed · requires Python >=3.9 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does pybreaker install cleanly?
Yes. In a fresh container with an empty cache, pip install pybreaker finished in 0.5s, leaving 1 package and 1 MB on disk. pip-audit reported no known vulnerabilities.
What does pybreaker need to run?
Python >=3.9, and nothing compiled: it is pure Python. In our run import pybreaker succeeded in 0.14s, and the package ships py.typed for type checkers.
pybreaker or circuitbreaker: which should you use?
circuitbreaker: Choose it for simple decorator-based circuits that do not need Redis coordination. PyBreaker 1.4.1 installed in 0.5 seconds as 1 MB and imported in 0.14 seconds in our sandbox, making it a cheap circuit state machine for synchronous Python services.
When should you not use pybreaker?
Protected calls are native async def. PyBreaker's asynchronous methods target Tornado generators and do not observe awaited asyncio failures.
Use it if
- A failing database, queue, or HTTP upstream keeps consuming workers with repeated doomed calls.
- One integration should share a failure threshold and recovery window across all its call sites.
- Multiple processes need the same circuit state and can rely on an existing Redis service.
- Operations will export transition callbacks plus the built-in failure and success counters.
- Protected calls are native `async def`. PyBreaker's asynchronous methods target Tornado generators and do not observe awaited asyncio failures.
- Errors are isolated transients rather than an outage pattern. Bounded retries with jitter describe that recovery behavior better.
- A gateway or mesh already trips the same upstream. Two circuit clocks can hide traffic and make recovery timing hard to explain.
- Every worker must open together but Redis cannot be used. Memory storage gives each process an independent state machine.
- Prometheus or OpenTelemetry output must be built in. This package supplies counters and inline listeners, leaving exporters to application code.
Setup reality
We installed pybreaker 1.4.1 in a clean Python 3.12 Bookworm sandbox. pip took 0.5 seconds, leaving 1 package and 1 MB on disk. The pure-Python distribution lists 7 direct dependencies, includes py.typed, and declares Python >=3.9. import pybreaker completed in 0.14 seconds, while pip-audit found 0 known vulnerabilities. Its metadata says BSD License. The README now states Python 3.10+, so 3.10 is the safer operational floor despite the looser package marker.
Keep one breaker alive for each dependency across requests. Constructing it inside a handler resets the failure counter every time and defeats the state machine. The protected network or database operation still needs its own timeout because a breaker cannot stop a call already blocked in I/O. Tune fail_max against actual failure bursts and exclude validation errors or expected 4xx responses that do not indicate an unhealthy upstream.
Default state is thread-safe and confined to one process. CircuitRedisStorage lets workers coordinate, but the README forbids decode_responses=True; enabling it produces AttributeError: 'str' object has no attribute 'decode'. Give every circuit a distinct Redis namespace or unrelated upstreams can share counters. Redis then sits in the protection path and needs separate monitoring.
Listener callbacks execute inline with the guarded call. Metrics and logging handlers must be quick and must not raise their own errors. The advertised asynchronous support covers Tornado's generator-coroutine convention through call_async and a decorator flag. It does not await a native coroutine's eventual exception, so asyncio services should use an await-aware breaker.
Patterns
Keep circuit state across requests create-breaker
import pybreaker
payments_breaker = pybreaker.CircuitBreaker(
name='payments-api',
fail_max=5,
reset_timeout=60,
)Module scope preserves counters. Constructing this object inside each request erases the history needed to open.
Decorate a database operation decorate-function
@payments_breaker
def charge(order_id, amount):
return payments_client.charge(order_id, amount, timeout=3)The database client needs its own timeout; circuit state cannot interrupt I/O already underway.
Guard code you cannot decorate call-explicitly
receipt = payments_breaker.call(
payments_client.charge,
order_id,
amount,
)`call` sends the callable and arguments through the same accounting without modifying third-party code.
Protect one integration block guard-block
with payments_breaker.calling():
response = client.post('/charges', json=payload, timeout=3)
response.raise_for_status()Every non-excluded exception inside this context counts. Do not mix unrelated business logic into the block.
Serve cached data from an open circuit fallback-open-circuit
try:
quote = pricing_breaker.call(fetch_quote, sku)
except pybreaker.CircuitBreakerError:
quote = cached_quote(sku)An open circuit skips the upstream entirely. The fallback must define acceptable stale and absent-data behavior.
Exclude validation failures exclude-business-error
orders_breaker = pybreaker.CircuitBreaker(
fail_max=5,
exclude=[InvalidCoupon, DuplicateOrder],
)Subclass instances are excluded as well. Count only exceptions that indicate the protected service is unhealthy.
Ignore HTTP client mistakes exclude-http-client-errors
api_breaker = pybreaker.CircuitBreaker(
exclude=[
lambda error: isinstance(error, HTTPError)
and error.response.status_code < 500
],
)The predicate sees each exception object, allowing expected 4xx responses to bypass the health counter.
Demand three healthy trial calls require-recovery
api_breaker = pybreaker.CircuitBreaker(
fail_max=5,
reset_timeout=60,
success_threshold=3,
)The half-open state remains until 3 accepted trials succeed; an earlier failure opens it again.
Share one circuit through Redis share-redis-state
import redis
client = redis.StrictRedis.from_url('redis://redis:6379/0')
storage = pybreaker.CircuitRedisStorage(
pybreaker.STATE_CLOSED, client, namespace='payments-api'
)
payments_breaker = pybreaker.CircuitBreaker(state_storage=storage)A decoded-response Redis client triggers the documented string `.decode` crash. The namespace must also be unique per upstream.
Emit transition telemetry listen-state-change
class MetricsListener(pybreaker.CircuitBreakerListener):
def state_change(self, breaker, old, new):
breaker_state.labels(breaker.name).set(new.name == 'closed')
payments_breaker.add_listeners(MetricsListener())This callback delays the protected call, so it should queue quick telemetry and swallow exporter failures.
Report counters and current state inspect-state
def breaker_status(breaker):
return {
'name': breaker.name,
'state': breaker.current_state,
'failures': breaker.fail_counter,
'successes': breaker.success_counter,
}An open value describes the dependency guard, not a dead application. Keep it out of the process liveness decision.
Trip the breaker manually force-open
payments_breaker.open()
# restore automatic probing when the upstream is ready
payments_breaker.half_open()With memory storage this change applies to one process. Redis storage is required for workers to observe the same state.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| circuitbreaker | PyPI | Choose it for simple decorator-based circuits that do not need Redis coordination. |
| aiobreaker | PyPI | Choose it when failures occur after awaiting native asyncio functions. |
| tenacity | PyPI | Choose it for transient retries with separately configured wait, stop, and error policies. |
| stamina | PyPI | Choose it for concise synchronous or asynchronous retries with preset backoff and jitter. |
More utils guides
lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

