interlock
A modern circuit breaker for Python — sync and async in a single class, sliding-window rate and slow-call detection, a type-safe API, and transparent integrations at the transport level.
Installation
uv add interlock-cb # or: pip install interlock-cb
interlock-cb supports Python 3.11 and newer. The core uses only the standard library; external integrations are installed as optional extras.
Quickstart
Create one named breaker per dependency and reuse it around every call to it:
from interlock import CircuitBreaker, CircuitOpenError, Config
payments = CircuitBreaker(
name='payments',
config=Config(
failure_rate_threshold=0.5, # trip at 50% failures...
minimum_number_of_calls=20, # ...once the window holds 20 calls
slow_call_duration_threshold=2.0, # a call slower than 2s counts as slow
slow_call_rate_threshold=0.3, # 30% slow calls trip it just as well
),
)
@payments
def charge(amount: int) -> str:
return gateway.charge(amount)
try:
receipt = charge(100)
except CircuitOpenError as exc:
print(exc) # Circuit 'payments' is open; retry in ~60.000s
The slow-call thresholds matter as much as the failure ones: a dependency that answers every call in 30 seconds raises nothing, so a consecutive-failure counter keeps the circuit closed while your own request queue fills up.
The same instance protects async callables — there is no second class to configure and no separate state to reason about:
@payments
async def refund(charge_id: str) -> None:
await gateway.refund(charge_id)
The decorator preserves the wrapped signature and whether it is sync or async.
breaker.call(fn, ...), with breaker and async with breaker protect the
same call in other shapes — see
Getting started for all
calling styles and
Configuration for
every threshold.
Why interlock
- Sync and async, one class.
CircuitBreakerdispatches to separate sync and async paths without duplicating the public API. - Failure rates over sliding windows. Choose count- or time-based windows instead of relying only on consecutive failures.
- Slow calls and returned values count. Detect latency degradation and classify unsuccessful results even when no exception is raised.
- Type-safe decorators. Wrapped signatures and their sync/async nature are
preserved; the package ships
py.typedand passes three strict type checkers. - Zero-dependency core. Optional clients, frameworks, storage and observability integrations never leak into the core package.
- Composable resilience. Combine timeout, bulkhead, breaker, retry and fallback explicitly, or coordinate breaker state across instances with Redis.
Safe production rollout
Start a new integration in METRICS_ONLY to observe real failure and slow-call
rates without rejecting traffic. The initial state is applied before a lazy
per-host breaker can admit its first request:
import httpx2
from interlock import Config, LoggingEventListener, State
from interlock.integrations.httpx2 import AsyncCircuitBreakerTransport
transport = AsyncCircuitBreakerTransport(
httpx2.AsyncHTTPTransport(),
initial_state=State.METRICS_ONLY,
config=Config(failure_rate_threshold=0.25, minimum_number_of_calls=50),
listener=LoggingEventListener(),
)
LoggingEventListener writes every event through stdlib logging; swap it for
an EventListener that exports to your metrics backend. Hosts are only known at
runtime, so transport.registry.items() lists every breaker created so far and
get_existing(host) inspects one without creating it. After tuning thresholds,
deploy a new transport with the default initial_state=State.CLOSED; the
enforcing instance starts with a fresh window. See
States and manual control.
Shared state across instances
A local breaker only reacts to what its own process saw. Back it with Redis and the whole fleet backs off together:
import redis
from interlock import CircuitBreaker
from interlock.integrations.redis import RedisStorage
payments = CircuitBreaker(
name='payments',
storage=RedisStorage(redis.Redis(host='redis.internal')),
)
Tripping is atomic across racing instances, recovery probes are budgeted globally rather than per process, and a Redis outage degrades to local state instead of failing calls. Sharing state gates traffic everywhere at once — that is the point, and the risk, so the Redis integration page starts with when not to use it.
Resilience pipeline
Compose strategies in an explicit order (first is outermost) while keeping the breaker useful as a standalone primitive:
from interlock import CircuitBreaker, CircuitOpenError, Pipeline
breaker = CircuitBreaker(name='recommendations')
pipeline = (
Pipeline.builder()
.fallback(lambda exc: [], on=(CircuitOpenError,))
.retry(attempts=4) # requires interlock-cb[tenacity]
.circuit_breaker(breaker)
.bulkhead(8)
.timeout(2.0)
.build()
)
@pipeline
async def fetch_picks(user: str) -> list[str]:
return await client.get_picks(user)
Retries never hammer an open circuit, one hung attempt cannot eat the retry budget, and every decision is observable — see the pipeline guide.
Integrations
The httpx2 transport applies one breaker per host with no decorators at call
sites:
import httpx2
from interlock.integrations.httpx2 import CircuitBreakerTransport
transport = CircuitBreakerTransport(httpx2.HTTPTransport())
client = httpx2.Client(transport=transport)
By default, transport exceptions and the canonical retryable statuses
(429, 500, 502, 503, 504) count as failures; 4xx client errors do not.
| Integration | Install | Documentation |
|---|---|---|
| httpx2 | interlock-cb[httpx2] |
Per-host transport |
| httpx | interlock-cb[httpx] |
Per-host transport |
| aiohttp | interlock-cb[aiohttp] |
Client middleware |
| requests | interlock-cb[requests] |
Session adapter |
| FastAPI | interlock-cb[fastapi] |
503 + Retry-After handler |
| Litestar | interlock-cb[litestar] |
503 + Retry-After handler |
| tenacity | interlock-cb[tenacity] |
Retry composition |
| Redis | interlock-cb[redis] |
Shared state |
| OpenTelemetry | interlock-cb[otel] |
Metrics listener |
The integrations overview also includes recipes for LLM SDKs and Flask/Django.
How it compares
interlock-cb is young: its first release was in 2026. Established libraries such as pybreaker and circuitbreaker have carried production traffic for years and remain a better fit when maturity matters more than the feature differences.
| Feature | interlock-cb | pybreaker | circuitbreaker |
|---|---|---|---|
| Core states (closed / open / half-open) | ✅ | ✅ | ✅ |
| Native asyncio | ✅ | Tornado | ✅ |
| Trip condition | failure rate | consecutive failures | consecutive failures |
| Time-based sliding window | ✅ | — | — |
| Slow-call detection | ✅ | — | — |
| Shared state across processes | ✅ | ✅ | — |
| Composable resilience pipeline | ✅ | — | — |
Fully typed API (py.typed) |
✅ | — | — |
The full comparison covers more features as well as aiobreaker and purgatory. Something out of date or unfair? Please open a PR.
The reliability work compensating for the project's shorter production history includes 100% branch coverage, three strict type checkers, mutation testing of the state machine and engine, property- and model-based tests, and CI on free-threaded CPython. The correctness and testing page documents what is verified and where the limits are.
Documentation
The full documentation is hosted at https://bagowix.github.io/interlock/. Start with:
- Getting started
- Configuration and states
- Timeouts and retries
- Resilience pipeline
- Integrations
- Correctness and testing
- API reference
For a deterministic, network-free demonstration of every state transition, run
the examples/
scripts or follow the
walkthrough.
Contributing
Bug reports and pull requests are welcome. See
CONTRIBUTING.md
for the local setup and the checks a change must pass, and
CODE_OF_CONDUCT.md
for community expectations. Security issues: please follow
SECURITY.md.
License
interlock is released under the MIT License.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file interlock_cb-2.6.1.tar.gz.
File metadata
- Download URL: interlock_cb-2.6.1.tar.gz
- Upload date:
- Size: 465.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dfb33c614fef4df8e5ac79056a7addef91f9efc1abca46cca048f32749a7881a
|
|
| MD5 |
0353d615b6edd5337610d5eb693b9ff0
|
|
| BLAKE2b-256 |
3e5271c0ea164e604b127a756424b5355eb6c74cb5913b112c9f1248635167df
|
Provenance
The following attestation bundles were made for interlock_cb-2.6.1.tar.gz:
Publisher:
release.yml on bagowix/interlock
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
interlock_cb-2.6.1.tar.gz -
Subject digest:
dfb33c614fef4df8e5ac79056a7addef91f9efc1abca46cca048f32749a7881a - Sigstore transparency entry: 2498857059
- Sigstore integration time:
-
Permalink:
bagowix/interlock@a12d792ebcd901e7906136c92fbac41244703520 -
Branch / Tag:
refs/tags/v2.6.1 - Owner: https://github.com/bagowix
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a12d792ebcd901e7906136c92fbac41244703520 -
Trigger Event:
push
-
Statement type:
File details
Details for the file interlock_cb-2.6.1-py3-none-any.whl.
File metadata
- Download URL: interlock_cb-2.6.1-py3-none-any.whl
- Upload date:
- Size: 81.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
33998a7e2277078469436d09e982354ae56088265b4ff55ff19a47c7d54920e4
|
|
| MD5 |
d0456417f897db013ff91f9c9ac4436e
|
|
| BLAKE2b-256 |
b62e9ee10c359ecbd97786c4f16dfb0330376c1dbc4e7919c37e54e005daddb7
|
Provenance
The following attestation bundles were made for interlock_cb-2.6.1-py3-none-any.whl:
Publisher:
release.yml on bagowix/interlock
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
interlock_cb-2.6.1-py3-none-any.whl -
Subject digest:
33998a7e2277078469436d09e982354ae56088265b4ff55ff19a47c7d54920e4 - Sigstore transparency entry: 2498857069
- Sigstore integration time:
-
Permalink:
bagowix/interlock@a12d792ebcd901e7906136c92fbac41244703520 -
Branch / Tag:
refs/tags/v2.6.1 - Owner: https://github.com/bagowix
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a12d792ebcd901e7906136c92fbac41244703520 -
Trigger Event:
push
-
Statement type: