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(*, name, config=None, clock=None, 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), orbreaker.call(fn, *args, **kwargs). - Properties:
name: str,state: State. 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 theauto_transitiontimer. Teardown, not a state change: neither closes the circuit (reset()does). Idempotent, and terminal — the lane never restarts. See Shutdown.storage— optional shared backend (StorageorAsyncStorage) 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.
Frozen dataclass of thresholds, window and timing; validated on construction.
See Configuration for every field. Raises
ValueError on invalid input.
Registry(*, config=None, clock=None, classifier=None, listener=None, storage=None)
registry.get(name, *, config=None) -> 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 storage
is handed to every breaker the registry creates; each coordinates under its own
name.
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.
State—CLOSED,OPEN,HALF_OPEN,FORCED_OPEN,DISABLED,METRICS_ONLY. AStrEnum; values are stable lowercase identifiers.Outcome—SUCCESS,FAILURE,SLOW_SUCCESS,SLOW_FAILURE, with.is_failureand.is_slowproperties.WindowType—COUNT_BASED,TIME_BASED.
Frozen dataclass: total_calls, failed_calls, slow_calls, plus
.failure_rate and .slow_call_rate properties (both 0.0 when empty).
InterlockError— base of all interlock errors.CircuitOpenError(breaker_name, *, retry_after=None, last_failure=None)— raised on rejection; attributesbreaker_name,retry_after,last_failure.CallTimeoutError(timeout)— raised bytimeoutandsync_timeout; attributetimeout.BulkheadFullError(max_concurrent, *, max_wait=0.0)— raised by a pipeline bulkhead when no concurrency slot frees up in time; attributesmax_concurrent,max_wait.InterlockDeprecationWarning— subclassesUserWarning, visible by default.
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.
Compose strategies around one call, outermost first — see the pipeline guide:
Pipeline(*strategies)— the executor; works as a signature-preserving decorator and aspipeline.call(fn, *args, **kwargs)(detect-dispatching, like the breaker's). No context manager by design.Pipeline.builder()/PipelineBuilder— step-by-step assembly:.fallback(...),.retry(...)(lazytenacityextra),.circuit_breaker(breaker),.bulkhead(...),.timeout(seconds),.add(custom),.build().Strategy— the structural protocol:execute(call)/execute_async(call);execute_asyncalways 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; raisesBulkheadFullError.FallbackStrategy(fallback, *, on=(Exception,), name='fallback', listener=None)— explicit substitution for selected failures; result typedT | F.RetryStrategy(...)— lives ininterlock.integrations.tenacity(see below).
Implement any of these to swap a core behaviour:
Clock—monotonic() -> float. Inject a fake for deterministic tests.SlidingWindow—record(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/closetake an optionalexpected_version(version-fenced CAS); every write carries attl. Mechanism only — threshold policy stays in the core.AsyncStorageis the awaitable mirror. See the Redis integration.FailureClassifier—is_failure(*, result, exception) -> bool. See Failure classification.EventListener—on_state_change,on_call,on_rejected,on_reset, pluson_storage_degraded/on_storage_recoveredfor coordinated breakers andon_retry/on_bulkhead_rejected/on_fallbackfor pipeline strategies (all optional hooks are dispatched only if present, so older listeners keep working). See Observability.
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 oflease_probe:granted: boolplus the post-attemptstate: SharedState.
LoggingEventListener(logger=None)— top-level; zero dependencies.interlock.integrations.otel.OTelEventListener(meter=None)— extrainterlock-cb[otel].
Extra interlock-cb[httpx2], module interlock.integrations.httpx2:
CircuitBreakerTransport(transport, *, config=None, clock=None, classifier=None, listener=None)AsyncCircuitBreakerTransport(transport, *, ...)HttpStatusClassifier(failure_statuses=None)— fails on transport exceptions and statuses429, 500, 502, 503, 504(override the set viafailure_statuses).
See the httpx2 integration.
Extra interlock-cb[aiohttp] (aiohttp ≥ 3.12), module interlock.integrations.aiohttp:
CircuitBreakerMiddleware(*, config=None, clock=None, classifier=None, listener=None)— client middleware forClientSession(middlewares=(...,)); one breaker per request host.HttpStatusClassifier(failure_statuses=None)— same policy as the httpx2 variant, readingClientResponse.status.
See the aiohttp integration.
Extra interlock-cb[requests], module interlock.integrations.requests:
CircuitBreakerAdapter(*, config=None, clock=None, classifier=None, listener=None, **adapter_kwargs)—HTTPAdaptersubclass forsession.mount(...); one breaker per request host. Extra kwargs go toHTTPAdapter.HttpStatusClassifier(failure_statuses=None)— same policy, readingResponse.status_code.
See the requests integration.
Extra interlock-cb[tenacity], module interlock.integrations.tenacity:
retry_unless_open(*transient)— tenacity retry predicate: retries the listed transient exception types (default: anyException), neverCircuitOpenError.wait_probe(fallback, *, jitter=0.1)— tenacity wait strategy: sleepsCircuitOpenError.retry_after(+ up tojitterseconds) after a rejection, delegates tofallbackotherwise.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,CircuitOpenErrornot retried by default.
See the tenacity integration and the retries guide.
Extra interlock-cb[fastapi], module interlock.integrations.fastapi:
breaker_dependency(name, *, registry)— returns aDepends-compatible callable yielding the named breaker from a sharedRegistry.install_exception_handler(app)— registers a handler mappingCircuitOpenErrorto503with aRetry-Afterheader.circuit_open_handler(request, exc)— the handler itself, for custom registration.
See the FastAPI integration.
Extra interlock-cb[litestar] (Litestar ≥ 2.23), module
interlock.integrations.litestar:
breaker_dependency(name, *, registry)— returns aProvideyielding the named breaker from a sharedRegistry; annotate handler parameters withNamedDependency[CircuitBreaker].circuit_open_handler(request, exc)— mapsCircuitOpenErrorto503with aRetry-Afterheader; pass it in the app'sexception_handlers.
See the Litestar integration.
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)— syncStorageover aredis.Redisclient.AsyncRedisStorage(client, *, ...)— async mirror overredis.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.