Skip to content

Timeout

A circuit breaker without a timeout is unsafe. A call that hangs forever is never counted as slow or failed — it just holds a resource indefinitely. timeout bounds an awaited block and turns a hang into a CallTimeoutError, which a surrounding breaker records as a (slow) failure.

from interlock import timeout

async with timeout(2.0):
    await client.get(url)        # raises CallTimeoutError after 2 seconds

Composing with a breaker

Compose timeout with a breaker manually — pipeline composition is a v2 feature. Put the timeout inside the protected callable so the breaker observes the CallTimeoutError:

from interlock import CircuitBreaker, timeout

breaker = CircuitBreaker(name='search')

@breaker
async def search(q: str) -> bytes:
    async with timeout(2.0):
        return await client.get('/search', params={'q': q})

Now a request that exceeds 2 seconds raises CallTimeoutError; the breaker counts it as a failure and, once the failure rate crosses the threshold, opens the circuit — converting slow hangs into fast rejections.

Synchronous code

timeout relies on asyncio cancelling the coroutine in place, which has no synchronous equivalent: a blocking call cannot be interrupted from outside its own thread, and signal.SIGALRM only works in the main thread, so it breaks in threaded servers. sync_timeout instead runs the callable in a daemon worker thread and joins it with a deadline. It is a decorator, so it wraps a callable rather than a block:

from interlock import CircuitBreaker, sync_timeout

breaker = CircuitBreaker(name='search')

@breaker
@sync_timeout(2.0)
def search(q: str) -> bytes:
    return client.get('/search', params={'q': q}).content

A call that exceeds 2 seconds raises CallTimeoutError, which the breaker records exactly as with the async path. The decorator preserves the wrapped function's signature, arguments and return value.

The worker keeps running after a timeout

Python cannot forcibly kill a thread. After sync_timeout raises, the worker thread keeps running in the background until the call returns on its own — it cannot be cancelled, so it may still hold the resource it was waiting on. The caller is unblocked immediately, but the underlying work is not stopped. Prefer the async timeout wherever you control an event loop; reach for sync_timeout only in genuinely synchronous code.

Why not bake it in?

interlock keeps retry, fallback and timeout as explicit, observable features rather than hidden magic inside the breaker. You decide the deadline at the call site, and the failure it produces flows through the same classification and metrics as any other.