Skip to content

retry

Re-run a call that failed with a transient error, waiting between attempts.

from nopanic import backoff, retry

@retry(
    attempts=5,                                       # total, including the first
    on=(ConnectionError, TimeoutError),               # or a predicate: lambda e: ...
    backoff=backoff.full_jitter(base=0.1, cap=30.0),  # the default strategy
    giveup=lambda e: getattr(e, "status", 0) == 401,  # never retry hopeless errors
    before_sleep=lambda a: log.warning("attempt %d failed: %s", a.attempt, a.exception),
)
def fetch(): ...

When attempts run out, the original exception is re-raised. There is no RetryError to unwrap.

Backoff strategies

Strategy Delays
backoff.fixed(delay=1.0) constant
backoff.exponential(base=0.1, factor=2.0, cap=30.0) deterministic growth
backoff.full_jitter(base=0.1, factor=2.0, cap=30.0) uniform(0, exponential): recommended for shared services
backoff.decorrelated_jitter(base=0.1, cap=30.0) uniform(base, previous x 3)
a number fixed seconds
any iterable of floats your own; retrying stops when it runs out

Retry-After

An exception with a numeric retry_after attribute (seconds) raises the wait to at least that long, plus a small margin (5% + 50 ms) so the server's window has really passed. CircuitOpen and RateLimited carry it; for HTTP clients, set it from the Retry-After header (see the HTTP guide). Server-sent hints are capped by retry_after_cap (default 60 s) so a hostile server cannot park your client. Disable with honor_retry_after=False.

Time budgets

max_time=20.0 bounds the whole retry loop: retrying stops as soon as the next wait would end past the budget, so no attempt ever starts late. An ambient deadline works the same way, across every policy inside it.

Retrying on results

Some clients return an error response instead of raising. on_result treats a return value as a failure:

@retry(attempts=4, on_result=lambda response: response.status_code in (429, 502, 503))
async def get(url):
    return await client.get(url)

When retrying stops, the last result is returned as-is. In before_sleep, RetryAttempt.result holds the rejected value and RetryAttempt.exception is None.

Retry budgets

retry(budget=...) shares a RetryBudget across calls and policies, so retries stay a small fraction of real traffic during an outage.

Streams

On a generator or async generator function, retry re-opens a stream that fails before its first item. After an item was delivered, a failure propagates: delivered items cannot be taken back. on_result does not apply to streams.

Events

retry.attempt_failed (attempt, delay, exception) before each wait and retry.gave_up (attempt, exception, reason) when it stops; reason is one of attempts, giveup, backoff, max_time, deadline, budget. A non-matching exception propagates silently.