Skip to content

Getting started

Install

pip install nopanic

No dependencies. Optional add-ons: nopanic-otel and nopanic-redis.

A policy is a decorator

Every policy is an object you apply as a decorator. It works the same on def, async def, and (for the policies where it makes sense) generator functions:

from nopanic import retry

@retry(attempts=3, on=ConnectionError)
def fetch_profile(user_id): ...

@retry(attempts=3, on=ConnectionError)
async def fetch_profile_async(user_id): ...

Stateful policies are shared on purpose

Breakers, bulkheads and rate limiters hold state. Create one instance per dependency and use it at every call site that talks to that dependency:

from nopanic import circuit_breaker, rate_limit

payments = circuit_breaker(name="payments", min_calls=10, reset_timeout=15.0)
payments_quota = rate_limit(100, per=1.0, name="payments")

@payments_quota
@payments
def charge(order): ...

@payments_quota
@payments
def refund(order): ...

Stack them in the right order

The decorator closest to the function runs innermost. A typical production stack, outermost to innermost:

from nopanic import CircuitOpen, backoff, compose, deadline, fallback, retry, timeout

resilient = compose(
    fallback(lambda exc: CACHED_ANSWER, on=(ConnectionError, TimeoutError, CircuitOpen)),
    deadline(10.0),                                   # the whole operation
    retry(attempts=3, on=(ConnectionError, TimeoutError), backoff=backoff.full_jitter(0.2)),
    payments,                                         # the breaker
    payments_quota,                                   # the rate limit
    timeout(2.0),                                     # each attempt
)

@resilient
def quote(order): ...

Read it inside out: each attempt gets 2 seconds, waits for a token, feeds the breaker; transient failures retry with jitter; the whole thing never exceeds 10 seconds; anything left becomes a graceful answer. See Composition.

No function to decorate?

user = retry(attempts=3).call(client.get_user, user_id)        # sync
user = await resilient.acall(async_client.get_user, user_id)    # async

call / acall exist on every policy and every compose stack.

What a policy raises

User exceptions are never wrapped: when retries run out you get the original exception back. Policies raise only:

Exception When
CircuitOpen The breaker rejects a call (has retry_after)
BulkheadFull No slot within max_wait
RateLimited No token within max_wait (has retry_after)
TimeoutError (builtin) A timeout expired

The first three subclass ResilienceError.