backoff

Python function decorators that retry calls on exceptions or bad return values, with exponential, fibonacci, or constant backoff and jitter.

Library
PyPI
v2.2.1
2,695 stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum. How we score it →
44 /100 Fair
Development Activity 0
Maintenance 20
Community 56
Maturity 60
Momentum 40

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation. How we score it →
76 /100 Good
Architecture 78
Code Quality 82
Innovation 55
Learning Curve 90

backoff is a small, focused Python library that provides function decorators for retrying unreliable operations. Rather than hand-rolling retry loops around network calls or polling logic, you wrap a function with @backoff.on_exception to retry when a specific exception is raised, or @backoff.on_predicate to retry until a return value satisfies a condition. Both decorators accept a wait generator (expo, fibo, constant, decay, or runtime) that controls the pacing between attempts, plus give-up conditions like max_tries and max_time.

The library supports both synchronous functions and asyncio coroutines with an identical interface, dispatching internally based on whether the decorated target is a coroutine function. Jitter functions (full_jitter, random_jitter) can be layered on top of any wait generator to avoid thundering-herd collisions across concurrent clients, and event handler hooks (on_success, on_backoff, on_giveup) let callers wire in custom logging or metrics without touching the core retry logic.

What You Get

  • on_exception decorator to retry a function when a specified exception (or tuple of exceptions) is raised
  • on_predicate decorator to retry until the function’s return value satisfies a condition, defaulting to a falsy check
  • Wait generators — expo, fibo, constant, decay, and runtime — for exponential, fibonacci, constant-interval, decay, and response-driven pacing
  • full_jitter and random_jitter functions to randomize wait times and avoid retry collisions across clients
  • Native support for both synchronous functions and asyncio coroutines with the same decorator API
  • Give-up controls via max_tries, max_time, and a custom giveup predicate function, plus raise_on_giveup to suppress the final exception

Common Use Cases

  • Retrying HTTP requests to flaky or rate-limited external APIs with exponential backoff
  • Polling a queue or resource for externally generated content using fibonacci or constant-interval backoff
  • Respecting a Retry-After response header via the runtime wait generator
  • Adding retry/backoff behavior to async I/O calls in asyncio-based services without separate sync/async code paths
  • Emitting structured logs or metrics on each backoff/giveup event via on_backoff and on_giveup handlers

Under The Hood

Architecture

The library is organized as small, single-purpose modules under backoff/: _decorator.py exposes the public on_exception/on_predicate decorators, which configure logging and handler lists then dispatch to either _sync.py or _async.py based on asyncio.iscoroutinefunction(target) — the same decorator API transparently drives two different retry-loop implementations. Shared plumbing (_maybe_call, _init_wait_gen, _next_wait, default log handlers) lives in _common.py so both retry loops stay in sync. Wait pacing is fully decoupled from retry control flow: _wait_gen.py defines each strategy as a plain Python generator advanced with .send(), so on_exception/on_predicate never need to know which strategy is active. This separation means the core abstraction to change if extending the library is the wait-generator protocol (a generator that yields on send(value)), not the retry loops themselves.

Tech Stack

Pure Python with no runtime dependencies beyond the standard library (asyncio, logging, functools, itertools, math, datetime). Targets Python 3.7+ per pyproject.toml, built with Poetry (poetry-core backend). Type hints are enforced via mypy and a dedicated _typing.py module defines internal type aliases (_CallableT, _Predicate, _WaitGenerator, etc.). Dev tooling is flake8 for linting, pytest with pytest-asyncio and pytest-cov for testing, and requests/responses are used only in the test suite for HTTP-retry examples, not as a library dependency.

Code Quality

The test suite is extensive relative to the library’s size — test_backoff.py and test_backoff_async.py alone total over 1,600 lines covering sync and async retry paths in parallel, with additional focused suites for jitter, wait generators, and static typing (test_typing.py exercises the type stubs directly). The Makefile’s check target chains flake8, mypy --show-error-codes, and pytest, so linting and type-checking are treated as first-class alongside tests, though a GitHub Actions CI workflow is limited to CodeQL scanning rather than running this full check suite on every push. Naming and error handling are consistent throughout: internal helpers are underscore-prefixed, public decorators validate and normalize handler arguments via _config_handlers before entering the retry loop.

API Design The two-decorator surface (on_exception, on_predicate) covers the overwhelming majority of retry scenarios with minimal boilerplate — a single decorator line with a wait generator and exception/predicate argument is enough to add production-grade retry behavior. Keyword-only arguments after the wait generator keep call sites readable, and accepting callables for max_tries/max_time allows runtime-evaluated configuration without extra ceremony. The identical API for sync and async code (the same @backoff.on_exception(backoff.expo, ...) line works on both) removes a common source of duplicated logic in codebases that mix sync and async call sites.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers