mrkeyoor.com_
Sun 20 Sept 11:43 UTC
PyPIUtilsupdated 20 Sept 2026

backoff review

backoff 2.2.1 puts retry rules on Python functions with decorators. It can react to a named exception or an unacceptable return value, then wait using exponential, Fibonacci, constant, or response-derived timing. The same API covers regular functions and asyncio coroutines. You can cap attempts or elapsed time, stop on a specific error, add jitter, and observe each retry through callbacks. Our sandbox found a typed, pure-Python install with no direct dependencies. The catch is ownership: litl/backoff is archived, and 2.2.1 has been the current release since October 2022.

Verdict

backoff 2.2.1 installed in 0.2 seconds, occupied 1 MB, and produced zero pip-audit findings in our sandbox, but its GitHub repository is archived. Keep it in tested existing code; pick a maintained retry library for a new service.

We installed it

Lab card: what happened when we installed backoffScreenshot of backoff documentation
Install✓ · 0.2s1 package on disk · 1 MB
Importimport backoff in 0.24s · pure Python · py.typed · requires Python >=3.7,<4.0
Known vulns0(pip-audit)

Answers from our run

Does backoff install cleanly?

Yes. In a fresh container with an empty cache, pip install backoff finished in 0.2s, leaving 1 package and 1 MB on disk. pip-audit reported no known vulnerabilities.

What does backoff need to run?

Python >=3.7,<4.0, and nothing compiled: it is pure Python. In our run import backoff succeeded in 0.24s, and the package ships py.typed for type checkers.

backoff or tenacity: which should you use?

tenacity: Choose it for an actively maintained retry API with decorator and block-level forms. backoff 2.2.1 installed in 0.2 seconds, occupied 1 MB, and produced zero pip-audit findings in our sandbox, but its GitHub repository is archived.

When should you not use backoff?

You are selecting a retry dependency for a new service. GitHub has made litl/backoff read-only, so fixes cannot land in this repository.

API stability5/5Version 2.2.1 retains the two central decorators, the documented wait generators, give-up predicates, jitter functions, stacked policies, and retry event handlers. Its only release note is a correction to a wait-generator type hint. The archive status makes an upstream breaking release improbable, though it also freezes bugs and compatibility assumptions.
Docs4/5The repository README explains exception retries, predicate polling, Retry-After timing, jitter semantics, decorator stacking, runtime option callables, event dictionaries, logging, and asyncio with working examples. It also documents the None return from raise_on_giveup=False. There is no separate maintained reference or a production checklist for operation timeouts and retry budgets.
Maintenance1/5GitHub reports that litl/backoff is archived, with the last push on May 2, 2024 and 64 open issues and pull requests. PyPI lists 2.2.1 from October 5, 2022 as the latest release. The package ran under our Python 3.12 image, but the read-only repository cannot accept a future runtime fix or publish normal maintenance from this line.
Ecosystem3/5The queue snapshot records 39,061,772 weekly downloads and GitHub reports 2,694 stars, which points to a large installed base. The package has no direct dependencies, carries typing metadata, and handles sync and asyncio callables. New projects have maintained choices such as Tenacity and Stamina, while an archived core gives integrations nowhere to send fixes.

Discussed on

  1. hnBackoff: Python function decorators for configurable backoff and retry49 points
  2. hnPython backoff repository was archived4 points

Use it if

  • You maintain a service that already expresses retry policy with on_exception or on_predicate decorators.
  • A polling function should repeat until its return value passes a small predicate.
  • One retry declaration needs to work on both synchronous functions and asyncio coroutines.
  • The next wait must come from a response value such as a Retry-After header.
Skip it if

Setup reality

We installed backoff 2.2.1 in 0.2 seconds in a fresh Python 3.12 container. The result was one package and 1 MB on disk, with zero direct dependencies. pip-audit found no known vulnerabilities. It is pure Python, declares Python 3.7 through 3.x, and includes py.typed. Our import backoff check completed in 0.24 seconds.

No account, credential, or configuration file is involved. Decorators are created when Python imports the module, though options such as max_time can receive a zero-argument callable. Use that form when the value lives in runtime settings. The package logger is silent by default through a NullHandler, so wire your own logging handler or retry callbacks if attempts must appear in operations data.

Put a ceiling on every retry. The basic on_exception form has no finite stop unless you set max_time or max_tries. Full jitter is enabled by default, and the generator's delay is the maximum sleep rather than an exact schedule. A finite retry window still needs an individual timeout on the network or storage call, because backoff does not impose one.

Coroutine functions are detected and awaited, and event handlers may be coroutines too. A blocking synchronous handler still stalls the event loop. The default final behavior raises the last exception after give-up handlers run. If raise_on_giveup is false, exhaustion becomes a None return, so the calling code must reserve None for that outcome.

Patterns

Retry one transient failure retry-transient-exception

import backoff

@backoff.on_exception(backoff.expo, ConnectionError, max_time=30)
def fetch_record():
    return client.fetch()

max_time limits total elapsed retry time. Without max_time or max_tries, the decorator can keep retrying a permanent fault.

Retry a fixed exception tuple retry-error-family

@backoff.on_exception(
    backoff.expo,
    (TimeoutError, ConnectionError),
    max_tries=5,
)
def fetch_record():
    return client.fetch()

Only the listed exception types trigger another attempt; every other exception leaves the function immediately.

Stop retrying client errors give-up-on-http-status

def fatal(error):
    response = getattr(error, 'response', None)
    return response is not None and 400 <= response.status_code < 500

@backoff.on_exception(backoff.expo, RequestError, giveup=fatal, max_time=60)
def load_items():
    return client.get('/items')

A truthy giveup result ends the loop. The last exception is still raised under the default raise_on_giveup setting.

Poll until a truthy value arrives poll-until-value

@backoff.on_predicate(backoff.constant, interval=1, jitter=None, max_time=20)
def next_message():
    return queue.read()

The default predicate retries every falsey result, including None, 0, False, and empty containers.

Repeat while a job is pending poll-specific-state

@backoff.on_predicate(
    backoff.fibo,
    predicate=lambda job: job.status == 'pending',
    max_value=8,
    max_time=60,
)
def read_job():
    return jobs.get(job_id)

max_value belongs to the Fibonacci wait generator; max_time caps the complete decorated call.

Take the delay from a response honor-retry-after

@backoff.on_predicate(
    backoff.runtime,
    predicate=lambda response: response.status_code == 429,
    value=lambda response: int(response.headers['Retry-After']),
    jitter=None,
    max_time=120,
)
def request():
    return client.get('/quota')

This parser accepts numeric Retry-After values only. Add HTTP-date parsing when the upstream can send that form.

Turn off random jitter use-fixed-delays

@backoff.on_exception(
    backoff.expo,
    ConnectionError,
    max_tries=4,
    jitter=None,
)
def request():
    return client.get('/health')

jitter=None makes delays deterministic, which can synchronize many clients hitting the same recovering service.

Resolve a limit at call time read-runtime-budget

def retry_budget():
    return settings.retry_seconds

@backoff.on_exception(backoff.expo, OSError, max_time=retry_budget)
def sync_file():
    return storage.sync()

Pass the function itself. Calling retry_budget inside the decorator would freeze the setting during module import.

Count each scheduled retry record-attempt

def record_retry(details):
    metrics.increment('worker.retry', tags={'tries': details['tries']})

@backoff.on_exception(backoff.expo, OSError, max_tries=4, on_backoff=record_retry)
def sync_file():
    return storage.sync()

on_backoff receives a details mapping with tries, elapsed time, target arguments, and the next wait.

Log the terminal failure record-exhaustion

def record_giveup(details):
    logger.error('retry exhausted', exc_info=details['exception'])

@backoff.on_exception(backoff.expo, OSError, max_tries=4, on_giveup=record_giveup)
def sync_file():
    return storage.sync()

The default policy raises the exception again after the give-up callback finishes.

Wrap an asyncio request retry-async-call

import aiohttp
import backoff

@backoff.on_exception(backoff.expo, aiohttp.ClientError, max_time=30)
async def fetch_json(session, url):
    async with session.get(url) as response:
        response.raise_for_status()
        return await response.json()

Use coroutine callbacks or quick synchronous callbacks here; blocking callback work holds up the event loop.

Return cached data after exhaustion fallback-after-giveup

@backoff.on_exception(
    backoff.expo, OSError, max_tries=3, raise_on_giveup=False
)
def read_remote():
    return storage.read()

result = read_remote()
if result is None:
    result = read_cache()

raise_on_giveup=False returns None after the last attempt. Do not use this shape when None is also a valid remote result.

Alternatives

PackageRegistryPick it when
tenacityPyPIChoose it for an actively maintained retry API with decorator and block-level forms.
staminaPyPIChoose it when Tenacity-backed retries should come with narrower defaults and instrumentation support.
retryingPyPIConsider it only for older code already written against its decorator conventions.

More utils guides

lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.