Skip to content

deadline

A timeout bounds one call. A deadline bounds a whole operation, however many retries, waits and nested calls it takes.

from nopanic import deadline, time_remaining

async with deadline(5.0):                 # or: with deadline(5.0):
    profile = await get_profile(uid)      # retries, timeouts, rate-limit waits...
    orders = await get_orders(uid)        # ...all share the same 5 seconds

The deadline is an ambient value (a contextvars variable): it follows your code into awaited coroutines, Trio tasks and timeout worker threads. Policies inside it cooperate:

Policy Inside a deadline
timeout Clamps its limit to the time remaining; fails at once if the deadline has passed
retry Gives up instead of sleeping into a wait that would outlast it (reason deadline)
rate_limit, adaptive_rate_limit, redis_rate_limit Reject with RateLimited instead of queueing past it
bulkhead Rejects with BulkheadFull instead of waiting past it

Nested deadlines can only shorten the budget, never extend it.

It cooperates, it does not interrupt

deadline declares a budget; it does not cancel anything by itself. To enforce it on a call, put a timeout inside: it uses whichever limit is tighter. Pass the rest to your own clients:

response = httpx.get(url, timeout=time_remaining())

time_remaining() returns the seconds left, or None outside a deadline.

As a policy

deadline is also a policy, giving each call its own budget:

@compose(deadline(10.0), retry(attempts=5), timeout(3.0))
def sync_inventory(): ...

One instance must not be entered as a with block twice at the same time (create one per block); as a decorator it can be shared freely. Generator functions are not supported: a context value set inside a generator would leak into the consumer between items.