Repository files navigation

interlock

PyPIDownloadsPython versionsLicense: MITCICoverageOpenSSF ScorecardOpenSSF Best PracticesCodSpeedDocumentationllms.txtContext7

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

Create one named breaker per dependency and reuse it around every call to it:

frominterlockimportCircuitBreaker, CircuitOpenError, Configpayments=CircuitBreaker(
name='payments',
config=Config(
failure_rate_threshold=0.5, # trip at 50% failures...minimum_number_of_calls=20, # ...once the window holds 20 callsslow_call_duration_threshold=2.0, # a call slower than 2s counts as slowslow_call_rate_threshold=0.3, # 30% slow calls trip it just as well
),
)
@paymentsdefcharge(amount: int) ->str:
returngateway.charge(amount)
try:
receipt=charge(100)
exceptCircuitOpenErrorasexc:
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:

@paymentsasyncdefrefund(charge_id: str) ->None:
awaitgateway.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:

importhttpx2frominterlockimportConfig, LoggingEventListener, Statefrominterlock.integrations.httpx2importAsyncCircuitBreakerTransporttransport=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:

importredisfrominterlockimportCircuitBreakerfrominterlock.integrations.redisimportRedisStoragepayments=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:

frominterlockimportCircuitBreaker, CircuitOpenError, Pipelinebreaker=CircuitBreaker(name='recommendations')
pipeline= (
Pipeline.builder()
.fallback(lambdaexc: [], on=(CircuitOpenError,))
.retry(attempts=4) # requires interlock-cb[tenacity]
.circuit_breaker(breaker)
.bulkhead(8)
.timeout(2.0)
.build()
)
@pipelineasyncdeffetch_picks(user: str) ->list[str]:
returnawaitclient.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:

importhttpx2frominterlock.integrations.httpx2importCircuitBreakerTransporttransport=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.

IntegrationInstallDocumentation
httpx2interlock-cb[httpx2]Per-host transport
httpxinterlock-cb[httpx]Per-host transport
aiohttpinterlock-cb[aiohttp]Client middleware
requestsinterlock-cb[requests]Session adapter
FastAPIinterlock-cb[fastapi]503 + Retry-After handler
Litestarinterlock-cb[litestar]503 + Retry-After handler
tenacityinterlock-cb[tenacity]Retry composition
Redisinterlock-cb[redis]Shared state
OpenTelemetryinterlock-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.

Featureinterlock-cbpybreakercircuitbreaker
Core states (closed / open / half-open)
Native asyncioTornado
Trip conditionfailure rateconsecutive failuresconsecutive 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.

About

Circuit breaker for Python: sync + async in one class, sliding-window rate, slow-call detection, Polly-style resilience pipeline, type-safe API

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

72 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

interlock

PyPIDownloadsPython versionsLicense: MITCICoverageOpenSSF ScorecardOpenSSF Best PracticesCodSpeedDocumentationllms.txtContext7

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

Create one named breaker per dependency and reuse it around every call to it:

frominterlockimportCircuitBreaker, CircuitOpenError, Configpayments=CircuitBreaker(
name='payments',
config=Config(
failure_rate_threshold=0.5, # trip at 50% failures...minimum_number_of_calls=20, # ...once the window holds 20 callsslow_call_duration_threshold=2.0, # a call slower than 2s counts as slowslow_call_rate_threshold=0.3, # 30% slow calls trip it just as well
),
)
@paymentsdefcharge(amount: int) ->str:
returngateway.charge(amount)
try:
receipt=charge(100)
exceptCircuitOpenErrorasexc:
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:

@paymentsasyncdefrefund(charge_id: str) ->None:
awaitgateway.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:

importhttpx2frominterlockimportConfig, LoggingEventListener, Statefrominterlock.integrations.httpx2importAsyncCircuitBreakerTransporttransport=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:

importredisfrominterlockimportCircuitBreakerfrominterlock.integrations.redisimportRedisStoragepayments=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:

frominterlockimportCircuitBreaker, CircuitOpenError, Pipelinebreaker=CircuitBreaker(name='recommendations')
pipeline= (
Pipeline.builder()
.fallback(lambdaexc: [], on=(CircuitOpenError,))
.retry(attempts=4) # requires interlock-cb[tenacity]
.circuit_breaker(breaker)
.bulkhead(8)
.timeout(2.0)
.build()
)
@pipelineasyncdeffetch_picks(user: str) ->list[str]:
returnawaitclient.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:

importhttpx2frominterlock.integrations.httpx2importCircuitBreakerTransporttransport=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.

IntegrationInstallDocumentation
httpx2interlock-cb[httpx2]Per-host transport
httpxinterlock-cb[httpx]Per-host transport
aiohttpinterlock-cb[aiohttp]Client middleware
requestsinterlock-cb[requests]Session adapter
FastAPIinterlock-cb[fastapi]503 + Retry-After handler
Litestarinterlock-cb[litestar]503 + Retry-After handler
tenacityinterlock-cb[tenacity]Retry composition
Redisinterlock-cb[redis]Shared state
OpenTelemetryinterlock-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.

Featureinterlock-cbpybreakercircuitbreaker
Core states (closed / open / half-open)
Native asyncioTornado
Trip conditionfailure rateconsecutive failuresconsecutive 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.

About

Circuit breaker for Python: sync + async in one class, sliding-window rate, slow-call detection, Polly-style resilience pipeline, type-safe API

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

72 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

interlock

PyPIDownloadsPython versionsLicense: MITCICoverageOpenSSF ScorecardOpenSSF Best PracticesCodSpeedDocumentationllms.txtContext7

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

Create one named breaker per dependency and reuse it around every call to it:

frominterlockimportCircuitBreaker, CircuitOpenError, Configpayments=CircuitBreaker(
name='payments',
config=Config(
failure_rate_threshold=0.5, # trip at 50% failures...minimum_number_of_calls=20, # ...once the window holds 20 callsslow_call_duration_threshold=2.0, # a call slower than 2s counts as slowslow_call_rate_threshold=0.3, # 30% slow calls trip it just as well
),
)
@paymentsdefcharge(amount: int) ->str:
returngateway.charge(amount)
try:
receipt=charge(100)
exceptCircuitOpenErrorasexc:
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:

@paymentsasyncdefrefund(charge_id: str) ->None:
awaitgateway.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:

importhttpx2frominterlockimportConfig, LoggingEventListener, Statefrominterlock.integrations.httpx2importAsyncCircuitBreakerTransporttransport=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:

importredisfrominterlockimportCircuitBreakerfrominterlock.integrations.redisimportRedisStoragepayments=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:

frominterlockimportCircuitBreaker, CircuitOpenError, Pipelinebreaker=CircuitBreaker(name='recommendations')
pipeline= (
Pipeline.builder()
.fallback(lambdaexc: [], on=(CircuitOpenError,))
.retry(attempts=4) # requires interlock-cb[tenacity]
.circuit_breaker(breaker)
.bulkhead(8)
.timeout(2.0)
.build()
)
@pipelineasyncdeffetch_picks(user: str) ->list[str]:
returnawaitclient.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:

importhttpx2frominterlock.integrations.httpx2importCircuitBreakerTransporttransport=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.

IntegrationInstallDocumentation
httpx2interlock-cb[httpx2]Per-host transport
httpxinterlock-cb[httpx]Per-host transport
aiohttpinterlock-cb[aiohttp]Client middleware
requestsinterlock-cb[requests]Session adapter
FastAPIinterlock-cb[fastapi]503 + Retry-After handler
Litestarinterlock-cb[litestar]503 + Retry-After handler
tenacityinterlock-cb[tenacity]Retry composition
Redisinterlock-cb[redis]Shared state
OpenTelemetryinterlock-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.

Featureinterlock-cbpybreakercircuitbreaker
Core states (closed / open / half-open)
Native asyncioTornado
Trip conditionfailure rateconsecutive failuresconsecutive 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.

About

Circuit breaker for Python: sync + async in one class, sliding-window rate, slow-call detection, Polly-style resilience pipeline, type-safe API

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

72 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

interlock

PyPIDownloadsPython versionsLicense: MITCICoverageOpenSSF ScorecardOpenSSF Best PracticesCodSpeedDocumentationllms.txtContext7

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

Create one named breaker per dependency and reuse it around every call to it:

frominterlockimportCircuitBreaker, CircuitOpenError, Configpayments=CircuitBreaker(
name='payments',
config=Config(
failure_rate_threshold=0.5, # trip at 50% failures...minimum_number_of_calls=20, # ...once the window holds 20 callsslow_call_duration_threshold=2.0, # a call slower than 2s counts as slowslow_call_rate_threshold=0.3, # 30% slow calls trip it just as well
),
)
@paymentsdefcharge(amount: int) ->str:
returngateway.charge(amount)
try:
receipt=charge(100)
exceptCircuitOpenErrorasexc:
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:

@paymentsasyncdefrefund(charge_id: str) ->None:
awaitgateway.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:

importhttpx2frominterlockimportConfig, LoggingEventListener, Statefrominterlock.integrations.httpx2importAsyncCircuitBreakerTransporttransport=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:

importredisfrominterlockimportCircuitBreakerfrominterlock.integrations.redisimportRedisStoragepayments=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:

frominterlockimportCircuitBreaker, CircuitOpenError, Pipelinebreaker=CircuitBreaker(name='recommendations')
pipeline= (
Pipeline.builder()
.fallback(lambdaexc: [], on=(CircuitOpenError,))
.retry(attempts=4) # requires interlock-cb[tenacity]
.circuit_breaker(breaker)
.bulkhead(8)
.timeout(2.0)
.build()
)
@pipelineasyncdeffetch_picks(user: str) ->list[str]:
returnawaitclient.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:

importhttpx2frominterlock.integrations.httpx2importCircuitBreakerTransporttransport=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.

IntegrationInstallDocumentation
httpx2interlock-cb[httpx2]Per-host transport
httpxinterlock-cb[httpx]Per-host transport
aiohttpinterlock-cb[aiohttp]Client middleware
requestsinterlock-cb[requests]Session adapter
FastAPIinterlock-cb[fastapi]503 + Retry-After handler
Litestarinterlock-cb[litestar]503 + Retry-After handler
tenacityinterlock-cb[tenacity]Retry composition
Redisinterlock-cb[redis]Shared state
OpenTelemetryinterlock-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.

Featureinterlock-cbpybreakercircuitbreaker
Core states (closed / open / half-open)
Native asyncioTornado
Trip conditionfailure rateconsecutive failuresconsecutive 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.

About

Circuit breaker for Python: sync + async in one class, sliding-window rate, slow-call detection, Polly-style resilience pipeline, type-safe API

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

72 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

interlock

PyPIDownloadsPython versionsLicense: MITCICoverageOpenSSF ScorecardOpenSSF Best PracticesCodSpeedDocumentationllms.txtContext7

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

Create one named breaker per dependency and reuse it around every call to it:

frominterlockimportCircuitBreaker, CircuitOpenError, Configpayments=CircuitBreaker(
name='payments',
config=Config(
failure_rate_threshold=0.5, # trip at 50% failures...minimum_number_of_calls=20, # ...once the window holds 20 callsslow_call_duration_threshold=2.0, # a call slower than 2s counts as slowslow_call_rate_threshold=0.3, # 30% slow calls trip it just as well
),
)
@paymentsdefcharge(amount: int) ->str:
returngateway.charge(amount)
try:
receipt=charge(100)
exceptCircuitOpenErrorasexc:
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:

@paymentsasyncdefrefund(charge_id: str) ->None:
awaitgateway.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:

importhttpx2frominterlockimportConfig, LoggingEventListener, Statefrominterlock.integrations.httpx2importAsyncCircuitBreakerTransporttransport=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:

importredisfrominterlockimportCircuitBreakerfrominterlock.integrations.redisimportRedisStoragepayments=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:

frominterlockimportCircuitBreaker, CircuitOpenError, Pipelinebreaker=CircuitBreaker(name='recommendations')
pipeline= (
Pipeline.builder()
.fallback(lambdaexc: [], on=(CircuitOpenError,))
.retry(attempts=4) # requires interlock-cb[tenacity]
.circuit_breaker(breaker)
.bulkhead(8)
.timeout(2.0)
.build()
)
@pipelineasyncdeffetch_picks(user: str) ->list[str]:
returnawaitclient.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:

importhttpx2frominterlock.integrations.httpx2importCircuitBreakerTransporttransport=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.

IntegrationInstallDocumentation
httpx2interlock-cb[httpx2]Per-host transport
httpxinterlock-cb[httpx]Per-host transport
aiohttpinterlock-cb[aiohttp]Client middleware
requestsinterlock-cb[requests]Session adapter
FastAPIinterlock-cb[fastapi]503 + Retry-After handler
Litestarinterlock-cb[litestar]503 + Retry-After handler
tenacityinterlock-cb[tenacity]Retry composition
Redisinterlock-cb[redis]Shared state
OpenTelemetryinterlock-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.

Featureinterlock-cbpybreakercircuitbreaker
Core states (closed / open / half-open)
Native asyncioTornado
Trip conditionfailure rateconsecutive failuresconsecutive 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.

About

Circuit breaker for Python: sync + async in one class, sliding-window rate, slow-call detection, Polly-style resilience pipeline, type-safe API

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

72 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

interlock

PyPIDownloadsPython versionsLicense: MITCICoverageOpenSSF ScorecardOpenSSF Best PracticesCodSpeedDocumentationllms.txtContext7

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

Create one named breaker per dependency and reuse it around every call to it:

frominterlockimportCircuitBreaker, CircuitOpenError, Configpayments=CircuitBreaker(
name='payments',
config=Config(
failure_rate_threshold=0.5, # trip at 50% failures...minimum_number_of_calls=20, # ...once the window holds 20 callsslow_call_duration_threshold=2.0, # a call slower than 2s counts as slowslow_call_rate_threshold=0.3, # 30% slow calls trip it just as well
),
)
@paymentsdefcharge(amount: int) ->str:
returngateway.charge(amount)
try:
receipt=charge(100)
exceptCircuitOpenErrorasexc:
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:

@paymentsasyncdefrefund(charge_id: str) ->None:
awaitgateway.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:

importhttpx2frominterlockimportConfig, LoggingEventListener, Statefrominterlock.integrations.httpx2importAsyncCircuitBreakerTransporttransport=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:

importredisfrominterlockimportCircuitBreakerfrominterlock.integrations.redisimportRedisStoragepayments=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:

frominterlockimportCircuitBreaker, CircuitOpenError, Pipelinebreaker=CircuitBreaker(name='recommendations')
pipeline= (
Pipeline.builder()
.fallback(lambdaexc: [], on=(CircuitOpenError,))
.retry(attempts=4) # requires interlock-cb[tenacity]
.circuit_breaker(breaker)
.bulkhead(8)
.timeout(2.0)
.build()
)
@pipelineasyncdeffetch_picks(user: str) ->list[str]:
returnawaitclient.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:

importhttpx2frominterlock.integrations.httpx2importCircuitBreakerTransporttransport=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.

IntegrationInstallDocumentation
httpx2interlock-cb[httpx2]Per-host transport
httpxinterlock-cb[httpx]Per-host transport
aiohttpinterlock-cb[aiohttp]Client middleware
requestsinterlock-cb[requests]Session adapter
FastAPIinterlock-cb[fastapi]503 + Retry-After handler
Litestarinterlock-cb[litestar]503 + Retry-After handler
tenacityinterlock-cb[tenacity]Retry composition
Redisinterlock-cb[redis]Shared state
OpenTelemetryinterlock-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.

Featureinterlock-cbpybreakercircuitbreaker
Core states (closed / open / half-open)
Native asyncioTornado
Trip conditionfailure rateconsecutive failuresconsecutive 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.

About

Circuit breaker for Python: sync + async in one class, sliding-window rate, slow-call detection, Polly-style resilience pipeline, type-safe API

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

72 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

interlock

PyPIDownloadsPython versionsLicense: MITCICoverageOpenSSF ScorecardOpenSSF Best PracticesCodSpeedDocumentationllms.txtContext7

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

Create one named breaker per dependency and reuse it around every call to it:

frominterlockimportCircuitBreaker, CircuitOpenError, Configpayments=CircuitBreaker(
name='payments',
config=Config(
failure_rate_threshold=0.5, # trip at 50% failures...minimum_number_of_calls=20, # ...once the window holds 20 callsslow_call_duration_threshold=2.0, # a call slower than 2s counts as slowslow_call_rate_threshold=0.3, # 30% slow calls trip it just as well
),
)
@paymentsdefcharge(amount: int) ->str:
returngateway.charge(amount)
try:
receipt=charge(100)
exceptCircuitOpenErrorasexc:
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:

@paymentsasyncdefrefund(charge_id: str) ->None:
awaitgateway.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:

importhttpx2frominterlockimportConfig, LoggingEventListener, Statefrominterlock.integrations.httpx2importAsyncCircuitBreakerTransporttransport=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:

importredisfrominterlockimportCircuitBreakerfrominterlock.integrations.redisimportRedisStoragepayments=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:

frominterlockimportCircuitBreaker, CircuitOpenError, Pipelinebreaker=CircuitBreaker(name='recommendations')
pipeline= (
Pipeline.builder()
.fallback(lambdaexc: [], on=(CircuitOpenError,))
.retry(attempts=4) # requires interlock-cb[tenacity]
.circuit_breaker(breaker)
.bulkhead(8)
.timeout(2.0)
.build()
)
@pipelineasyncdeffetch_picks(user: str) ->list[str]:
returnawaitclient.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:

importhttpx2frominterlock.integrations.httpx2importCircuitBreakerTransporttransport=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.

IntegrationInstallDocumentation
httpx2interlock-cb[httpx2]Per-host transport
httpxinterlock-cb[httpx]Per-host transport
aiohttpinterlock-cb[aiohttp]Client middleware
requestsinterlock-cb[requests]Session adapter
FastAPIinterlock-cb[fastapi]503 + Retry-After handler
Litestarinterlock-cb[litestar]503 + Retry-After handler
tenacityinterlock-cb[tenacity]Retry composition
Redisinterlock-cb[redis]Shared state
OpenTelemetryinterlock-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.

Featureinterlock-cbpybreakercircuitbreaker
Core states (closed / open / half-open)
Native asyncioTornado
Trip conditionfailure rateconsecutive failuresconsecutive 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.

About

Circuit breaker for Python: sync + async in one class, sliding-window rate, slow-call detection, Polly-style resilience pipeline, type-safe API

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

72 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

interlock

PyPIDownloadsPython versionsLicense: MITCICoverageOpenSSF ScorecardOpenSSF Best PracticesCodSpeedDocumentationllms.txtContext7

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

Create one named breaker per dependency and reuse it around every call to it:

frominterlockimportCircuitBreaker, CircuitOpenError, Configpayments=CircuitBreaker(
name='payments',
config=Config(
failure_rate_threshold=0.5, # trip at 50% failures...minimum_number_of_calls=20, # ...once the window holds 20 callsslow_call_duration_threshold=2.0, # a call slower than 2s counts as slowslow_call_rate_threshold=0.3, # 30% slow calls trip it just as well
),
)
@paymentsdefcharge(amount: int) ->str:
returngateway.charge(amount)
try:
receipt=charge(100)
exceptCircuitOpenErrorasexc:
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:

@paymentsasyncdefrefund(charge_id: str) ->None:
awaitgateway.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:

importhttpx2frominterlockimportConfig, LoggingEventListener, Statefrominterlock.integrations.httpx2importAsyncCircuitBreakerTransporttransport=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:

importredisfrominterlockimportCircuitBreakerfrominterlock.integrations.redisimportRedisStoragepayments=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:

frominterlockimportCircuitBreaker, CircuitOpenError, Pipelinebreaker=CircuitBreaker(name='recommendations')
pipeline= (
Pipeline.builder()
.fallback(lambdaexc: [], on=(CircuitOpenError,))
.retry(attempts=4) # requires interlock-cb[tenacity]
.circuit_breaker(breaker)
.bulkhead(8)
.timeout(2.0)
.build()
)
@pipelineasyncdeffetch_picks(user: str) ->list[str]:
returnawaitclient.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:

importhttpx2frominterlock.integrations.httpx2importCircuitBreakerTransporttransport=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.

IntegrationInstallDocumentation
httpx2interlock-cb[httpx2]Per-host transport
httpxinterlock-cb[httpx]Per-host transport
aiohttpinterlock-cb[aiohttp]Client middleware
requestsinterlock-cb[requests]Session adapter
FastAPIinterlock-cb[fastapi]503 + Retry-After handler
Litestarinterlock-cb[litestar]503 + Retry-After handler
tenacityinterlock-cb[tenacity]Retry composition
Redisinterlock-cb[redis]Shared state
OpenTelemetryinterlock-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.

Featureinterlock-cbpybreakercircuitbreaker
Core states (closed / open / half-open)
Native asyncioTornado
Trip conditionfailure rateconsecutive failuresconsecutive 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.

About

Circuit breaker for Python: sync + async in one class, sliding-window rate, slow-call detection, Polly-style resilience pipeline, type-safe API

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

72 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages