Skip to main content

interlock

PyPI Downloads Python versions License: MIT CI Coverage OpenSSF Scorecard OpenSSF Best Practices CodSpeed Documentation llms.txt Context7 skills.sh

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.

CLOSED passes calls through and trips to OPEN once the failure or slow-call rate reaches its threshold; OPEN rejects calls without touching the dependency and moves to HALF_OPEN after the wait duration; HALF_OPEN admits probes only — a failing probe re-opens the circuit, successful probes close it

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

With a coding agent

The repository ships an Agent Skill that walks a coding agent through adding interlock to a codebase: an inventory of outbound calls, the integration to use for each, threshold sizing, a shadow-mode rollout, tests driven by a fake clock, and the migration from pybreaker or circuitbreaker. It installs into Claude Code, Cursor, Codex, GitHub Copilot, Gemini CLI and the other agents that read the open skills format:

npx skills add bagowix/interlock

Then ask the agent to add circuit breakers to a service. Agents that read documentation directly can use llms.txt, the fully inlined llms-full.txt or Context7.

By hand

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. CircuitBreaker dispatches 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.typed and 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:

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.

Metadata

Release files for interlock-cb 2.8.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for interlock-cb 2.8.1
File Size Uploaded
interlock_cb-2.8.1.tar.gz 501.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for interlock-cb 2.8.1
File Interpreter ABI Platform
interlock_cb-2.8.1-py3-none-any.whl Python 3 none any Details

Total release size: 594.4 kB

Release files / interlock_cb-2.8.1.tar.gz

Download URL interlock_cb-2.8.1.tar.gz
Size 501.7 kB
Tags Source
SHA-256 checksum
How to use checksums
4fecade5d9659f3ce97bdf5165af69911ae506f3e004fce4b11fa6a250357edf
BLAKE2b-256 checksum
How to use checksums
a9205bc29545bb6c095374e3281ef8599600ee354a75db1f9cd0c482dc2152ab
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 18, 2026.

Transparency log

Release files / interlock_cb-2.8.1-py3-none-any.whl

Download URL interlock_cb-2.8.1-py3-none-any.whl
Size 92.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c18f926f1ae6e4960a354e2a1841e6424abfffe55a8fe337b321f466190cbb4f
BLAKE2b-256 checksum
How to use checksums
73f70a107d7b38d46b6de9e4ef5b152a37733676dab7742da38bc6dba2ef1ba5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 18, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

2.8.1 This release

2 release files

2.8.0

2 release files

2.7.0

2 release files

2.6.1

2 release files

2.6.0

2 release files

2.5.0

2 release files

2.4.0

2 release files

2.3.0

2 release files

2.2.0

2 release files

2.1.4

2 release files

2.1.3

2 release files

2.1.2

2 release files

2.1.1

2 release files

2.1.0

2 release files

2.0.0

2 release files

1.3.0

2 release files

1.2.0

2 release files

1.1.0

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page