Retry Libraries & Tenacity — Core Concepts

Why retries matter

Distributed systems fail. A database might be momentarily overloaded, a DNS lookup could time out, or a third-party API might return a 503 for a few seconds during a deployment. Without retries, a single transient blip becomes a user-visible error.

The challenge isn’t retrying — it’s retrying correctly. Bad retry logic causes thundering herds (thousands of clients retrying simultaneously), wastes resources on permanent failures, and hides bugs that should surface immediately.

Tenacity fundamentals

Tenacity is the standard retry library in the Python ecosystem, replacing the older retrying package (which is unmaintained). It works as a decorator or a context manager and composes small, reusable pieces.

The core building blocks:

  • Stop conditions — when to give up (after N attempts, after X seconds, or both)
  • Wait strategies — how long to pause between attempts (fixed, exponential, random)
  • Retry conditions — which exceptions or return values trigger a retry
  • Callbacks — what to do before retrying, after giving up, or on success

Exponential backoff

The most important wait strategy doubles the delay each time: 1s → 2s → 4s → 8s. This gives the failing service time to recover without hammering it.

Raw exponential backoff has a problem though: if 500 clients all start retrying at the same second, they’ll all retry at 1s, 2s, 4s — synchronized waves that keep overloading the server. This is the thundering herd problem.

Jitter solves thundering herds

Adding randomness (“jitter”) to the wait time desynchronizes clients. Instead of exactly 4 seconds, one client waits 3.2 seconds and another waits 4.7 seconds. The server sees a smooth stream instead of synchronized bursts.

Tenacity supports several jitter approaches. Full jitter (random between 0 and the calculated delay) provides the best distribution according to AWS’s research on retry behavior.

Knowing when NOT to retry

This is where most retry implementations fail. Retrying a 401 Unauthorized error will never succeed — the credentials are wrong. Retrying a 400 Bad Request is equally pointless. Only transient errors deserve retries:

  • Retry: 429 (rate limited), 502/503/504 (server issues), connection timeouts, DNS failures
  • Don’t retry: 400 (bad input), 401/403 (auth problems), 404 (not found), 409 (conflict)

Tenacity lets you specify exactly which exceptions to retry and which to let bubble up immediately.

Stop conditions prevent infinite loops

Every retry strategy needs a circuit breaker. Common combinations:

  • Stop after 5 attempts (protects against truly broken services)
  • Stop after 60 seconds total (prevents user-facing requests from hanging)
  • Stop after 5 attempts OR 60 seconds, whichever comes first

Without stop conditions, a retry loop on a permanently broken service becomes a slow memory leak and thread blocker.

Retry vs. circuit breaker

Retries handle transient failures on individual requests. Circuit breakers handle persistent failures across a service. When a service fails 10 times in a row, a circuit breaker “opens” and stops sending any requests for a cooldown period. The two patterns are complementary: retry within a call, circuit-break across calls.

Common misconception

Developers often think retries add reliability for free. In reality, retries increase the total load on a failing system. If a service is struggling under load, 1,000 clients each retrying 3 times means 3,000 extra requests hitting an already overloaded server. This is why backoff, jitter, and circuit breakers must accompany retries — without them, retries make outages worse.

Tenacity vs. alternatives

  • tenacity — the most full-featured, actively maintained, supports async
  • backoff — simpler API, fewer features, good for basic cases
  • stamina — newer, opinionated (less configuration), built on tenacity internally
  • urllib3.Retry — built into the HTTP stack, limited to HTTP errors only

For most Python projects, tenacity is the right choice. It handles sync, async, and custom retry logic.

The one thing to remember: Good retries need three ingredients — exponential backoff to give systems time to recover, jitter to prevent thundering herds, and clear stop conditions to avoid retrying forever.

pythonreliabilitylibraries

See Also

  • Python Aiohttp Client Understand Aiohttp Client through a practical analogy so your Python decisions become faster and clearer.
  • Python Api Client Design Why building your own API client in Python is like creating a TV remote that only has the buttons you actually need.
  • Python Api Documentation Swagger Swagger turns your Python API into an interactive playground where anyone can click buttons to try it out — no coding required.
  • Python Api Mocking Responses Why testing with fake API responses is like rehearsing a play with stand-ins before the real actors show up.
  • Python Api Pagination Clients Why APIs send data in pages, and how Python handles it — like reading a book one chapter at a time instead of swallowing the whole thing.