Skip to content

API reference

Everything below is importable from the top-level interlock package, except the integration adapters, which live in their own modules to keep the core dependency-free.

CircuitBreaker

CircuitBreaker(*, name, config=None, clock=None, initial_state=State.CLOSED,
               classifier=None, listener=None, storage=None)

A named breaker for sync and async callables.

  • Use as a decorator (@breaker), a sync/async context manager (with / async with), or breaker.call(fn, *args, **kwargs).
  • call_sync(fn, ...) / call_async(fn, ...) — the same protection with the sync/async dispatch skipped, for callers that already know their own nature (the transport integrations use them per request). call_sync never awaits: a coroutine function passed to it is recorded as an immediate success. call_async awaits whatever fn returns, so it accepts any awaitable-returning callable, not only a coroutine function.
  • Properties: name: str, state: State.
  • initial_state — one of CLOSED, FORCED_OPEN, DISABLED or METRICS_ONLY; transitional OPEN / HALF_OPEN raise ValueError.
  • snapshot() -> WindowSnapshot — current, self-consistent window aggregates; concurrent call settlement cannot expose a partially updated window.
  • Manual control: reset(), force_open(), disable(), metrics_only().
  • close() / aclose() — release background resources: the coordinator lane and the auto_transition timer. Teardown, not a state change: neither closes the circuit (reset() does). Idempotent, and terminal — the lane never restarts. See Shutdown.
  • storage — optional shared backend (Storage or AsyncStorage) for coordinated state across instances; see the Redis integration. A coordinated breaker matches its storage's runtime (sync storage → sync API, async storage → async API); without a storage the breaker stays fully dual.

Config

Frozen dataclass of thresholds, window and timing; validated on construction. See Configuration for every field. Raises ValueError on invalid input.

Registry

Registry(*, config=None, clock=None, initial_state=State.CLOSED,
         classifier=None, listener=None, storage=None)
registry.get(name, *, config=None) -> CircuitBreaker
registry.get_existing(name) -> CircuitBreaker | None
registry.names() -> tuple[str, ...]
registry.items() -> tuple[tuple[str, CircuitBreaker], ...]
registry.close_all() / await registry.aclose_all()

Creates and caches named breakers. The same name always returns the same instance; the per-call config override applies only at creation. A cache hit is served without taking the registry lock, so a registry shared by every request of a service does not serialise on lookups. A storage is handed to every breaker the registry creates; each coordinates under its own name. initial_state is assigned before a lazy breaker is published; get_existing() inspects the cache without creating a missing name.

names() and items() enumerate the breakers created so far — the only way to see the ones the HTTP transports create lazily, one per host. Both take a point-in-time copy under the registry lock: a breaker created afterwards is not in it, and the returned tuple never changes.

for name, breaker in registry.items():
    print(name, breaker.state, breaker.snapshot())

close_all() / aclose_all() close every breaker created so far. The cache is kept, so get() keeps returning the same, torn-down instances instead of silently starting a fresh lane after shutdown.

Enums

  • StateCLOSED, OPEN, HALF_OPEN, FORCED_OPEN, DISABLED, METRICS_ONLY. A StrEnum; values are stable lowercase identifiers.
  • OutcomeSUCCESS, FAILURE, SLOW_SUCCESS, SLOW_FAILURE, with .is_failure and .is_slow properties.
  • WindowTypeCOUNT_BASED, TIME_BASED.

WindowSnapshot

Frozen dataclass: total_calls, failed_calls, slow_calls, plus .failure_rate and .slow_call_rate properties (both 0.0 when empty).

Errors & warnings

  • InterlockError — base of all interlock errors.
  • CircuitOpenError(breaker_name, *, retry_after=None, last_failure=None) — raised on rejection; attributes breaker_name, retry_after, last_failure.
  • CallTimeoutError(timeout) — raised by timeout and sync_timeout; attribute timeout.
  • BulkheadFullError(max_concurrent, *, max_wait=0.0) — raised by a pipeline bulkhead when no concurrency slot frees up in time; attributes max_concurrent, max_wait.
  • InterlockDeprecationWarning — subclasses UserWarning, visible by default.

Every HTTP client integration retypes the rejection — and only the rejection — as a subclass that is also a native error of the host library, so the idiom that library teaches and except CircuitOpenError catch the same error. The native base differs per integration; one except clause covers one client, not all four:

Integration Rejection type Native base
httpx CircuitOpenTransportError httpx.TransportError
httpx2 CircuitOpenTransportError httpx2.TransportError
aiohttp CircuitOpenClientError aiohttp.ClientConnectionError
requests CircuitOpenRequestError requests.exceptions.ConnectionError

CallTimeoutError and BulkheadFullError are paired only by the httpx and httpx2 transports, as CallTimeoutTransportError (a TimeoutException) and BulkheadFullTransportError (a PoolTimeout); they surface a timeout or bulkhead raised by a layer inside the wrapped transport. The aiohttp middleware and the requests adapter leave both untouched — neither hierarchy has an honest type for "no local slot was free" — so they reach the caller as plain interlock errors. See the integration sections below.

timeout / sync_timeout

async with timeout(seconds):
    ...  # async block


@sync_timeout(seconds)  # synchronous callable
def work(): ...

timeout is an async context manager that raises CallTimeoutError if the block exceeds seconds. sync_timeout is a decorator that runs a synchronous callable in a daemon worker thread and raises CallTimeoutError if it overruns seconds; the worker keeps running after a timeout (Python cannot kill a thread). See Timeout.

Pipeline

Compose strategies around one call, outermost first — see the pipeline guide:

  • Pipeline(*strategies) — the executor; works as a signature-preserving decorator and as pipeline.call(fn, *args, **kwargs) (detect-dispatching, like the breaker's). No context manager by design.
  • Pipeline.builder() / PipelineBuilder — step-by-step assembly: .fallback(...), .retry(...) (lazy tenacity extra), .circuit_breaker(breaker), .bulkhead(...), .timeout(seconds), .add(custom), .build().
  • Strategy — the structural protocol: execute(call) / execute_async(call); execute_async always receives a real coroutine function.
  • CircuitBreakerStrategy(breaker) — wraps a standalone breaker unchanged.
  • TimeoutStrategy(seconds) — bounds every attempt via the v1 primitives.
  • BulkheadStrategy(max_concurrent, *, max_wait=0.0, name='bulkhead', listener=None) — concurrency cap; raises BulkheadFullError.
  • FallbackStrategy(fallback, *, on=(Exception,), name='fallback', listener=None) — explicit substitution for selected failures; result typed T | F.
  • RetryStrategy(...) — lives in interlock.integrations.tenacity (see below).

Protocols (extension points)

Implement any of these to swap a core behaviour:

  • Clockmonotonic() -> float. Inject a fake for deterministic tests.
  • SlidingWindowrecord(outcome), snapshot() -> WindowSnapshot.
  • Storage / AsyncStorage — shared-state backend as atomic intent operations: read, trip_open, begin_half_open_if_elapsed, lease_probe, record_probe, close. trip_open/close take an optional expected_version (version-fenced CAS); every write carries a ttl. Mechanism only — threshold policy stays in the core. AsyncStorage is the awaitable mirror. See the Redis integration.
  • FailureClassifieris_failure(*, result, exception) -> bool, where exception is an Exception or None — cancellation and shutdown are released without being classified. See Failure classification.
  • EventListeneron_state_change, on_call, on_rejected, on_reset, plus on_storage_degraded / on_storage_recovered / on_storage_write_dropped for coordinated breakers and on_retry / on_bulkhead_rejected / on_fallback for pipeline strategies (all optional hooks are dispatched only if present, so older listeners keep working). See Observability.

Shared-state types

  • SharedState — frozen snapshot of one breaker's coordinated state: state, opened_at (backend time), version (for fencing), and the HALF_OPEN probe accounting (probes_permitted, probes_remaining, probes_completed, probe_failures, probe_slows). SharedState.closed() is the baseline an absent key implies.
  • ProbeLease — result of lease_probe: granted: bool plus the post-attempt state: SharedState.

Listeners

  • LoggingEventListener(logger=None) — top-level; zero dependencies.
  • interlock.integrations.otel.OTelEventListener(meter=None) — extra interlock-cb[otel].

httpx2 adapters

Extra interlock-cb[httpx2], module interlock.integrations.httpx2:

  • CircuitBreakerTransport(transport, *, config=None, clock=None, initial_state=State.CLOSED, classifier=None, listener=None, registry=None, name_resolver=<request host>)
  • AsyncCircuitBreakerTransport(transport, *, ...)
  • HttpStatusClassifier(*, failure_statuses=None, excluded_exceptions=None) — fails on transport exceptions and statuses 429, 500, 502, 503, 504 (override the set via failure_statuses). UnsupportedProtocol and LocalProtocolError are caller-side and count as successes; replace that set via excluded_exceptions.
  • CircuitOpenTransportError(breaker_name, *, retry_after=None, last_failure=None, request=None) — the rejection: an httpx2.TransportError and a CircuitOpenError.
  • CallTimeoutTransportError(timeout, *, request=None) — an httpx2.TimeoutException and a CallTimeoutError.
  • BulkheadFullTransportError(max_concurrent, *, max_wait=0.0, request=None) — an httpx2.PoolTimeout and a BulkheadFullError.

Both transports expose their per-host registry and the guarded transport as a read-only wrapped. close() / aclose() release the wrapped transport and, when the transport owns the registry, every breaker in it; a caller-owned registry stays open and is closed by its owner. Every adapter below takes the same registry option — a caller-owned Registry shared between clients — and combining it with any breaker-construction option (config, clock, initial_state, classifier, listener) raises ValueError, since the registry already owns them. name_resolver maps each request to its breaker name and defaults to the request host. See the httpx2 integration.

httpx adapters

Extra interlock-cb[httpx] (httpx ≥ 0.27.0), module interlock.integrations.httpx:

  • CircuitBreakerTransport(transport, *, config=None, clock=None, initial_state=State.CLOSED, classifier=None, listener=None, registry=None, name_resolver=<request host>)
  • AsyncCircuitBreakerTransport(transport, *, ...)
  • HttpStatusClassifier(*, failure_statuses=None, excluded_exceptions=None) — same policy, including the caller-side UnsupportedProtocol / LocalProtocolError exclusions.
  • CircuitOpenTransportError, CallTimeoutTransportError, BulkheadFullTransportError — the same three dialect errors as the httpx2 adapters, built on httpx.TransportError, httpx.TimeoutException and httpx.PoolTimeout.

Both transports expose their per-host registry and the guarded transport as a read-only wrapped, and preserve streaming responses. close() / aclose() release the wrapped transport and, when the transport owns the registry, every breaker in it; a caller-owned registry stays open and is closed by its owner. See the httpx integration.

aiohttp adapters

Extra interlock-cb[aiohttp] (aiohttp ≥ 3.12), module interlock.integrations.aiohttp:

  • CircuitBreakerMiddleware(*, config=None, clock=None, initial_state=State.CLOSED, classifier=None, listener=None, registry=None, name_resolver=<request host>) — client middleware for ClientSession(middlewares=(...,)); one breaker per request host.
  • HttpStatusClassifier(*, failure_statuses=None, excluded_exceptions=None) — same canonical HTTP policy, reading ClientResponse.status; nothing is excluded by default (aiohttp rejects bad URLs before the middleware runs).
  • CircuitOpenClientError(breaker_name, *, retry_after=None, last_failure=None) — the rejection: an aiohttp.ClientConnectionError and a CircuitOpenError.

It exposes its registry; call await middleware.aclose() during application shutdown. See the aiohttp integration.

requests adapters

Extra interlock-cb[requests], module interlock.integrations.requests:

  • CircuitBreakerAdapter(*, config=None, clock=None, initial_state=State.CLOSED, classifier=None, listener=None, registry=None, name_resolver=<request host>, **adapter_kwargs)HTTPAdapter subclass for session.mount(...); one breaker per request host. Extra kwargs go to HTTPAdapter.
  • HttpStatusClassifier(*, failure_statuses=None, excluded_exceptions=None) — same policy, reading Response.status_code; the caller-side InvalidURL counts as a success.
  • CircuitOpenRequestError(breaker_name, *, retry_after=None, last_failure=None, request=None) — the rejection: a requests.exceptions.ConnectionError and a CircuitOpenError; carries requests' own .request and .response.

It exposes its registry; closing the adapter or owning session releases both the connection pools and breaker resources. See the requests integration.

tenacity helpers

Extra interlock-cb[tenacity], module interlock.integrations.tenacity:

  • retry_unless_open(*transient) — tenacity retry predicate: retries the listed transient exception types (default: any Exception), never CircuitOpenError.
  • wait_probe(fallback, *, jitter=0.1) — tenacity wait strategy: sleeps CircuitOpenError.retry_after (+ up to jitter seconds) after a rejection, delegates to fallback otherwise.
  • RetryStrategy(*, attempts=3, retry=None, wait=None, sleep=None, async_sleep=None, before_sleep=None, name='retry', listener=None) — a bounded retry layer for the pipeline: policy delegated to tenacity, attempts always capped, the original exception re-raised when the budget runs out, CircuitOpenError not retried by default.

See the tenacity integration and the retries guide.

FastAPI adapters

Extra interlock-cb[fastapi], module interlock.integrations.fastapi:

  • breaker_dependency(name, *, registry) — returns a Depends-compatible callable yielding the named breaker from a shared Registry.
  • install_exception_handler(app) — registers a handler mapping CircuitOpenError to 503 with a Retry-After header.
  • circuit_open_handler(request, exc) — the handler itself, for custom registration.

See the FastAPI integration.

Litestar adapters

Extra interlock-cb[litestar] (Litestar ≥ 2.23), module interlock.integrations.litestar:

  • breaker_dependency(name, *, registry) — returns a Provide yielding the named breaker from a shared Registry; annotate handler parameters with NamedDependency[CircuitBreaker].
  • circuit_open_handler(request, exc) — maps CircuitOpenError to 503 with a Retry-After header; pass it in the app's exception_handlers.

See the Litestar integration.

Redis adapters

Extra interlock-cb[redis], module interlock.integrations.redis:

  • RedisStorage(client, *, key_prefix='interlock:cb:', state_ttl=300.0, poll_interval=1.0, retry_backoff=5.0, retry_backoff_multiplier=1.0, retry_backoff_max=None, retry_jitter=0.0, write_queue_size=128) — sync Storage over a redis.Redis client.
  • AsyncRedisStorage(client, *, ...) — async mirror over redis.asyncio.Redis.

One Redis hash per breaker; every transition is a Lua script (atomic across racing instances), elapse checks use the server's TIME. Works against Redis (5.0+), Valkey, or any RESP-compatible server.

See the Redis integration.