tenacity
tenacity adds configurable retry logic to any callable - exponential backoff, jitter, stop conditions, and hooks - without hand-rolling while loops around flaky network or database calls.
Search across all documentation pages
tenacity adds configurable retry logic to any callable - exponential backoff, jitter, stop conditions, and hooks - without hand-rolling while loops around flaky network or database calls.
from tenacity import retry, stop_after_attempt, wait_exponential_jitter
@retry(stop=stop_after_attempt(3), wait=wait_exponential_jitter(initial=1, max=10))
def fetch_status() -> dict:
response = httpx.get("https://api.example.com/health", timeout=5.0)
response.raise_for_status()
return response.json()When to reach for this:
from __future__ import annotations
import logging
import httpx
from tenacity import (
before_sleep_log,
retry,
retry_if_exception_type,
stop_after_attempt,
wait_exponential_jitter,
)
logger = logging.getLogger(__name__)
class TransientError(Exception):
pass
def _raise_for_transient(response: httpx.Response) -> None:
if response.status_code >= 500:
raise TransientError(f"server error {response.status_code}")
response.raise_for_status()
@retry(
retry=retry_if_exception_type((TransientError, httpx.TimeoutException)),
stop=stop_after_attempt(5),
wait=wait_exponential_jitter(initial=0.5, max=8),
before_sleep=before_sleep_log(logger, logging.WARNING),
reraise=True,
)
def charge_customer(customer_id: str, amount_cents: int) -> str:
with httpx.Client(timeout=10.0) as client:
response = client.post(
"/charges",
json={"customer_id": customer_id, "amount_cents": amount_cents},
headers={"Idempotency-Key": f"{customer_id}:{amount_cents}"},
)
_raise_for_transient(response)
return response.json()["id"]
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
print(charge_customer("cus_1", 1999))What this demonstrates:
TransientError and timeouts - not 4xx client mistakesbefore_sleep_log surfaces retry attempts in logsreraise=True preserves the final exception after exhaustion@retry wraps sync functions; use @retry on async functions with async-compatible wait/stop (or AsyncRetrying context).stop_after_attempt, stop_after_delay, stop_any.retry_if_exception, retry_if_result, retry_unless_exception_type.| Error Type | Retry? | Notes |
|---|---|---|
| HTTP 408/429/502/503/504 | Often | Respect Retry-After when present |
| HTTP 400/401/404 | No | Fix request or auth |
| DB deadlock | Yes | Short backoff, limited attempts |
| ValidationError | No | Data will not change on retry |
from tenacity import Retrying
# Imperative style inside a loop body
for attempt in Retrying(stop=stop_after_attempt(3)):
with attempt:
do_work()stop_after_attempt plus alert on final failure.Exception - Masks programming bugs. Fix: narrow retry_if_exception_type.wait_exponential_jitter.reraise=True and metrics on retry count.| Alternative | Use When | Don't Use When |
|---|---|---|
Manual for loop | One-off scripts with 2 retries | Shared policies across services |
urllib3.util.retry | Only requests/urllib3 stack | httpx-native apps |
Celery autoretry_for | Task queue retries at worker level | In-process HTTP client calls |
| Circuit breaker (pybreaker) | Sustained upstream failure | Brief transient blips only |
Yes - use async retry helpers or AsyncRetrying depending on tenacity version; keep one event loop per client.
Raise a domain TransientError after inspecting response.status_code inside the wrapped function.
Use retry_error_callback or wrap the call and handle the re-raised exception in the caller.
Start with 3-5 for HTTP; tune from p99 latency and upstream SLO - log retry metrics to decide.
Both layers are valid - client retries handle sub-second blips; Celery retries handle process restarts.
Mock the flaky dependency to fail twice then succeed; assert call count equals three.
Not automatically - parse the header in before_sleep and return a custom wait if needed.
Pass a no-op retry policy via dependency injection or set an env flag that returns the bare function.
Retry at the unit-of-work boundary after session.rollback() - do not retry mid-transaction.
The decorator is re-entrant per call; share no mutable state inside retried functions without locks.
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.
Reviewed by Chris St. John·Last updated Jul 19, 2026