{"id":51758056,"url":"https://github.com/llucs/backon","last_synced_at":"2026-07-22T18:00:51.114Z","repository":{"id":367853710,"uuid":"1282525624","full_name":"Llucs/backon","owner":"Llucs","description":"Function decoration for backoff and retry — modern, fast, and zero dependencies","archived":false,"fork":false,"pushed_at":"2026-07-18T03:15:56.000Z","size":1342,"stargazers_count":34,"open_issues_count":26,"forks_count":3,"subscribers_count":3,"default_branch":"main","last_synced_at":"2026-07-19T09:12:48.279Z","etag":null,"topics":["async","backoff","circuit-breaker","decorator","exponential-backoff","hedging","python","rate-limiting","resilience","retry"],"latest_commit_sha":null,"homepage":"https://pypi.org/project/backon/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Llucs.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-06-27T22:18:36.000Z","updated_at":"2026-07-18T13:15:21.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/Llucs/backon","commit_stats":null,"previous_names":["llucs/backon"],"tags_count":20,"template":false,"template_full_name":null,"purl":"pkg:github/Llucs/backon","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Llucs%2Fbackon","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Llucs%2Fbackon/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Llucs%2Fbackon/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Llucs%2Fbackon/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Llucs","download_url":"https://codeload.github.com/Llucs/backon/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Llucs%2Fbackon/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35771493,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-07-20T02:08:10.276Z","status":"online","status_checked_at":"2026-07-22T02:00:06.236Z","response_time":124,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["async","backoff","circuit-breaker","decorator","exponential-backoff","hedging","python","rate-limiting","resilience","retry"],"created_at":"2026-07-19T09:07:19.727Z","updated_at":"2026-07-22T18:00:51.106Z","avatar_url":"https://github.com/Llucs.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg\n    src=\"https://raw.githubusercontent.com/Llucs/backon/main/assets/circle_logo.svg\"\n    width=\"160\"\n    alt=\"backon logo\"\n  /\u003e\n\u003c/p\u003e\n\n\u003ch1 align=\"center\"\u003ebackon\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003e\n  Function decoration for backoff and retry — modern, fast, zero dependencies.\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://github.com/Llucs/backon/actions/workflows/ci.yml\"\u003e\n    \u003cimg src=\"https://github.com/Llucs/backon/actions/workflows/ci.yml/badge.svg\" /\u003e\n  \u003c/a\u003e\n\n  \u003ca href=\"https://github.com/Llucs/backon/actions/workflows/codeql.yml\"\u003e\n    \u003cimg src=\"https://github.com/Llucs/backon/actions/workflows/codeql.yml/badge.svg\" /\u003e\n  \u003c/a\u003e\n\n  \u003ca href=\"https://codecov.io/gh/Llucs/backon\"\u003e\n    \u003cimg src=\"https://codecov.io/gh/Llucs/backon/branch/main/graph/badge.svg\" /\u003e\n  \u003c/a\u003e\n\n  \u003ca href=\"https://pypi.org/project/backon/\"\u003e\n    \u003cimg src=\"https://img.shields.io/pypi/v/backon.svg\" /\u003e\n  \u003c/a\u003e\n\n  \u003ca href=\"https://pypi.org/project/backon/\"\u003e\n    \u003cimg src=\"https://img.shields.io/pypi/pyversions/backon.svg\" /\u003e\n  \u003c/a\u003e\n\n  \u003ca href=\"https://github.com/Llucs/backon/blob/main/LICENSE\"\u003e\n    \u003cimg src=\"https://img.shields.io/pypi/l/backon.svg\" /\u003e\n  \u003c/a\u003e\n\n  \u003ca href=\"https://pepy.tech/projects/backon\"\u003e\n    \u003cimg src=\"https://static.pepy.tech/personalized-badge/backon?period=total\u0026units=INTERNATIONAL_SYSTEM\u0026left_color=GRAY\u0026right_color=GREEN\u0026left_text=downloads\" /\u003e\n  \u003c/a\u003e\n\u003c/p\u003e\n\nbackon is a modern evolution of [backoff](https://github.com/litl/backoff) — a zero-dependency Python library for retry with exponential backoff. It provides decorator, functional, and context manager APIs for both sync and async code.\n\n![backon demo](https://raw.githubusercontent.com/Llucs/backon/main/assets/demo.gif)\n\n---\n\n## Table of Contents\n\n- [Features](#features)\n- [Installation](#installation)\n- [Quick Start](#quick-start)\n- [API Reference](#api-reference)\n  - [Decorators](#decorators)\n  - [Functional API](#functional-api)\n  - [Context Manager](#context-manager)\n  - [Callers](#callers)\n- [Wait Generators](#wait-generators)\n- [Stop Conditions](#stop-conditions)\n- [Retry Conditions](#retry-conditions)\n- [Jitter](#jitter)\n- [Handlers](#handlers)\n- [Global Toggle](#global-toggle)\n- [Generator Support](#generator-support)\n- [Async Support](#async-support)\n- [Custom Sleep](#custom-sleep)\n- [Advanced Features](#advanced-features)\n  - [Circuit Breaker](#circuit-breaker)\n  - [Hedging](#hedging)\n  - [Metrics](#metrics)\n  - [Testing Utilities](#testing-utilities)\n  - [Trio Support](#trio-support)\n  - [Retry Context Inspection](#retry-context-inspection)\n  - [Dynamic Backoff](#dynamic-backoff)\n  - [Hot Loop Detection](#hot-loop-detection)\n  - [Retry Statistics](#retry-statistics)\n  - [Operator Composition](#operator-composition)\n  - [Iterator API](#iterator-api)\n- [Migrating from backoff](#migrating-from-backoff)\n- [Contributing](#contributing)\n- [License](#license)\n\n---\n\n## Features\n\n- **Zero dependencies** — pure Python, stdlib only\n- **Four APIs** — decorator (`@on_exception`, `@on_predicate`), functional (`retry()`), context manager (`Retrying`), callable (`RetryingCaller` / `AsyncRetryingCaller`)\n- **Async native** — same API works for `async def` functions\n- **Generator native** — sync and async generators are retried transparently\n- **Full type hints** — validated with mypy, py.typed included\n- **Global toggle** — `backon.disable()` / `backon.enable()` for testing\n- **Custom sleep** — inject your own sleep function (useful for testing with `asyncio.Event`)\n- **Multiple wait strategies** — exponential, constant, Fibonacci, decay, runtime, randomized, incremental, and composable chains\n- **`wait_combine()`** — sum multiple wait strategies per retry step\n- **`retry_with()`** — override retry parameters per-call on decorated functions\n- **Jitter** — full jitter, random jitter, or none\n- **Rich callbacks** — `on_attempt`, `on_backoff`, `on_success`, `on_giveup`, `before_sleep`, `before`, `after`\n- **Circuit breaker** — CLOSED/OPEN/HALF_OPEN states with automatic recovery\n- **Hedging** — concurrent retry requests, first-success-wins\n- **Prometheus / OpenTelemetry / structlog metrics** — optional, zero hard dependencies\n- **Testing module** — `disable_retries()`, `limit_retries()`, `remove_backoff()`, `assert_retried()`\n- **Trio support** — retry with the trio async framework\n- **Operator overloading** — compose stops with `|` / `\u0026`, wait generators with `+`\n- **Iterator API** — `for attempt in Retrying(...):`\n- **Modern packaging** — PEP 621, PDM, py.typed\n\n---\n\n## Installation\n\n```bash\npip install backon\n```\n\nRequires Python 3.10+.\n\n---\n\n## Quick Start\n\n### Retry on exception\n\n```python\nimport backon\n\n@backon.on_exception(backon.expo, ValueError, max_tries=3)\ndef fetch_data():\n    return api.call()\n```\n\n### Retry on predicate\n\n```python\n@backon.on_predicate(backon.constant, max_tries=5, interval=0.5)\ndef poll_status():\n    return check_ready()\n```\n\n### Functional API\n\n```python\nresult = backon.retry(\n    fetch_data,\n    backon.expo,\n    exception=ValueError,\n    max_tries=3,\n)\n```\n\n### Context manager\n\n```python\nwith backon.Retrying(backon.expo, exception=ValueError, max_tries=3) as r:\n    result = r.call(fetch_data)\n```\n\nAsync variant:\n\n```python\nasync with backon.Retrying(backon.constant, exception=ValueError, max_tries=3, interval=0.5) as r:\n    result = await r.async_call(fetch_data)\n```\n\nThe `with` / `async with` block is purely a scoping device: it returns the `Retrying` object so you can call `r.call(...)` / `await r.async_call(...)`, which drives the retry loop. `__exit__` / `__aexit__` do not re-raise the final exception or otherwise interact with the loop — that happens inside `r.call` / `r.async_call`. Use `r.statistics` after the call if you need attempt counts. The same applies to `BreakerRetrying` and `HedgingRetrying` context managers.\n\n---\n\n## API Reference\n\n### Decorators\n\n#### `@backon.on_exception(wait_gen, exception, ...)`\n\nRetry when the decorated function raises one of the specified exceptions.\n\n```python\n@backon.on_exception(backon.expo, (ValueError, TimeoutError), max_tries=5)\ndef fetch():\n    ...\n```\n\n| Argument | Type | Default | Description |\n|---|---|---|---|\n| `wait_gen` | `WaitGenerator` | — | Wait strategy (expo, constant, fibo, etc.) |\n| `exception` | `type` or `tuple[type]` | — | Exception class(es) to retry on |\n| `max_tries` | `int` or `Callable[[], int]` | `None` | Maximum number of attempts |\n| `max_time` | `float`, `timedelta`, or `Callable` | `None` | Maximum total elapsed time |\n| `jitter` | `Jitterer` or `None` | `full_jitter` | Jitter function |\n| `giveup` | `Callable[[Exception], bool or float]` | `lambda e: False` | Stop retrying for matching exceptions; return `float` to override wait |\n| `on_success` | `Handler` or list | `None` | Called after successful attempt |\n| `on_backoff` | `Handler` or list | `None` | Called before each retry |\n| `on_giveup` | `Handler` or list | `None` | Called when retries exhausted |\n| `on_attempt` | `Handler` or list | `None` | Called before each attempt |\n| `before_sleep` | `Handler` or list | `None` | Called before sleeping |\n| `before` | `Handler` or list | `None` | Called before each attempt (lower-level than on_attempt) |\n| `after` | `Handler` or list | `None` | Called after each attempt (lower-level than on_success/on_giveup) |\n| `retry_error_callback` | `Callable[[dict], Any]` | `None` | Called when retry gives up instead of raising |\n| `raise_on_giveup` | `bool` | `True` | Raise final exception when giving up |\n| `logger` | `str` or `Logger` | `\"backon\"` | Logger name or instance |\n| `backoff_log_level` | `int` | `logging.INFO` | Log level for backoff messages |\n| `giveup_log_level` | `int` | `logging.ERROR` | Log level for giveup messages |\n| `sleep` | `Callable[[float], Any]` | `None` | Custom sleep function |\n| `rate_limit` | `RateLimiter` or `None` | `None` | Rate limiter to throttle retry calls |\n| `attempt_timeout` | `float` or `None` | `None` | Maximum time in seconds for a single attempt |\n| `**wait_gen_kwargs` | varies | — | Extra kwargs passed to the wait generator (e.g. `base=3`, `interval=0.5`) |\n\n#### `@backon.on_predicate(wait_gen, predicate, ...)`\n\nRetry while the predicate matches the return value.\n\n```python\n@backon.on_predicate(backon.constant, predicate=lambda x: x is None, max_tries=5)\ndef poll():\n    ...\n```\n\nAccepts all parameters from `on_exception` except `exception` and `giveup`. Adds:\n\n| Argument | Type | Default | Description |\n|---|---|---|---|\n| `predicate` | `Callable[[Any], bool]` | `operator.not_` | Retry when this returns `True` for the return value |\n\n#### `decorator.retry_with(**overrides)`\n\nEvery decorated function (sync, async, generator) exposes a `.retry_with()` method that returns a new decorated function with overridden parameters:\n\n```python\n@backon.on_exception(backon.expo, ValueError, max_tries=5)\ndef fetch():\n    ...\n\n# Override max_tries\nwrapped = fetch.retry_with(max_tries=3)\n\n# Override jitter\nwrapped = fetch.retry_with(jitter=None)\n\n# Override wait generator and sleep\nwrapped = fetch.retry_with(wait_gen=backon.constant, interval=0.1, sleep=lambda s: None)\n```\n\nThe original decorated function is unaffected.\n\n### Functional API\n\n#### `backon.retry(target, wait_gen, ...)`\n\n```python\nresult = backon.retry(\n    target=my_function,\n    wait_gen=backon.expo,\n    exception=ValueError,\n    max_tries=3,\n)\n```\n\nAccepts all parameters from `on_exception` plus `on_predicate` extras, plus:\n\n| Argument | Type | Default | Description |\n|---|---|---|---|\n| `condition` | `RetryCondition` | `None` | Advanced retry condition object |\n| `stop` | `Stop` | `None` | Advanced stop condition object |\n| `name` | `str` | `\"\"` | Identifier for the retry call |\n| `**wait_gen_kwargs` | varies | — | Extra kwargs passed to the wait generator |\n\nIf `target` is a coroutine function, `retry()` returns a coroutine. Otherwise it returns the result synchronously.\n\n### Context Manager\n\n#### `backon.Retrying(wait_gen, ...)`\n\n```python\nwith backon.Retrying(backon.expo, exception=ValueError, max_tries=3) as r:\n    r.call(my_function)\n\nasync with backon.Retrying(backon.constant, exception=ValueError, max_tries=3, interval=0.5) as r:\n    await r.async_call(my_async_function)\n```\n\n| Method | Description |\n|---|---|\n| `call(target, *args, **kwargs)` | Execute synchronously |\n| `async_call(target, *args, **kwargs)` | Execute asynchronously |\n| `copy()` | Return a modified copy of the Retrying instance |\n| `statistics` | Property returning dict with `attempt_number`, `elapsed`, `idle_for`, `start_time` |\n| `call_state` | Property returning the current `RetryCallState` |\n| `enabled` | Property to enable/disable retry per-instance |\n\n**Arguments:** Same as `retry()`, plus `enabled` (default `True`).\n\n### Callers\n\n#### `backon.RetryingCaller(wait_gen, ...)`\n\nA callable object with pre-bound exception type via `.on()`. Accepts the same configuration options as `Retrying` (`giveup`, `predicate`, `condition`, `stop`, the `on_*` handlers, `retry_error_callback`, `raise_on_giveup`, `logger`, `before`/`after`, etc.), forwarding them to the underlying retry loop on every call.\n\n```python\ncaller = backon.RetryingCaller(backon.expo, max_tries=3)\ncaller = caller.on(ValueError)\n\nresult = caller(my_function, arg1, arg2)\n```\n\n#### `backon.AsyncRetryingCaller(wait_gen, ...)`\n\nAsync variant of `RetryingCaller` — same configuration surface, async dispatch.\n\n```python\ncaller = backon.AsyncRetryingCaller(backon.expo, max_tries=3).on(ValueError)\nresult = await caller(my_async_function, arg1, arg2)\n```\n\n| Method | Description |\n|---|---|\n| `.on(exception)` | Return a copy bound to the given exception type |\n| `.copy()` | Return a modified copy |\n| `.__call__(target, *args, **kwargs)` | Execute with retry |\n\n---\n\n## Wait Generators\n\nAll wait generators are callables that produce a sequence of wait times. Pass extra kwargs (e.g. `interval=0.5`, `base=3`) as `**wait_gen_kwargs` to decorators and functions.\n\n| Generator | Signature | Description |\n|---|---|---|\n| `expo` | `(base=2, factor=1, max_value=None)` | Exponential backoff: `factor * base^n` |\n| `constant` | `(interval=1)` | Fixed interval; accepts `float` or `Sequence[float]` for varied intervals |\n| `fibo` | `(max_value=None)` | Fibonacci sequence: 1, 1, 2, 3, 5, 8, ... |\n| `runtime` | `(value=Callable)` | Dynamic wait from return value or exception — useful for `Retry-After` headers |\n| `decay` | `(initial_value=1, decay_factor=1, min_value=None)` | Exponential decay: `initial * e^(-t * decay_factor)` |\n| `wait_random_exponential` | `(multiplier=1, max_value=None, exp_base=2, min_value=0)` | Randomized exponential (uniform random between 0 and the exponential value) |\n| `wait_incrementing` | `(start=1, increment=1, max_value=None)` | Linear increment: `start + n * increment` |\n| `wait_chain` | `(*generators)` | Sequentially play through multiple generators |\n| `wait_combine` | `(*generators)` | Sum all generator wait values per step (unlike `+` which chains sequentially) |\n| `wait_exception` | `(value=Callable)` | Dynamic wait based on the caught exception |\n| `wait_random` | `(min=0, max=1)` | Uniform random wait between min and max |\n| `wait_exponential_jitter` | `(initial=1, max=60, exp_base=2, jitter=1)` | Exponential backoff with added random jitter |\n| `wait_none` | `()` | Always returns 0 (no wait) |\n\n**Composition:** Combine wait generators with `+` (sequential chain) or `wait_combine` (sum per step):\n\n```python\n# Sequential: wait_chain(expo, constant)\nwait_strategy = backon.expo(base=3) + backon.constant(interval=0.5)\n\n# Sum per step: wait_combine(expo, constant) — same kwargs to all sub-generators\nfrom backon import wait_combine\n\n@backon.on_exception(wait_combine(backon.expo, backon.constant), ValueError, max_tries=3)\ndef fetch():\n    ...\n```\n\n`wait_combine` calls all sub-generators with the same kwargs on each step and returns the sum — useful when you want combined behaviors on every retry rather than a sequence.\n\n---\n\n## Stop Conditions\n\nStop conditions determine when retry should cease. They can be composed with `|` (any) and `\u0026` (all).\n\n| Condition | Description |\n|---|---|\n| `stop_after_attempt(max_attempts)` | Stop after N attempts |\n| `stop_after_delay(max_delay)` | Stop after total elapsed time exceeds `max_delay` seconds |\n| `stop_before_delay(max_delay)` | Stop if the upcoming wait would make `elapsed + wait \u003e= max_delay` |\n| `stop_all(*stops)` | Stop when all sub-conditions are met |\n| `stop_any(*stops)` | Stop when any sub-condition is met |\n| `stop_never()` | Never stop (retry indefinitely) |\n| `stop_when_event_set(event)` | Stop when a `threading.Event` is set |\n\n```python\nfrom backon import stop_after_attempt, stop_after_delay, stop_any\n\nstop = stop_after_attempt(5) | stop_after_delay(30.0)\n```\n\n---\n\n## Retry Conditions\n\nRetry conditions determine *whether* a retry should happen. They can be composed with `|` and `\u0026`.\n\n| Condition | Description |\n|---|---|\n| `retry_if_exception_type(exc_types)` | Retry if exception is an instance of given type(s) — accepts a single type or a tuple of types |\n| `retry_if_exception(predicate)` | Retry if the exception matches a custom predicate |\n| `retry_if_exception_message(message, match=None)` | Retry if exception message contains a string (or matches regex with `match=\"re\"`) |\n| `retry_if_result(predicate)` | Retry if the return value matches a predicate |\n| `retry_if_not_result(predicate)` | Retry if the return value does NOT match a predicate |\n| `retry_all(*conditions)` | Retry only when all conditions pass |\n| `retry_any(*conditions)` | Retry when any condition passes |\n| `retry_always()` | Always retry |\n| `retry_never()` | Never retry |\n| `retry_if_exception_cause_type(exc_types)` | Retry if the exception's cause chain matches the given type(s) |\n| `retry_if_not_exception_type(exc_types)` | Retry if exception is NOT an instance of the given type(s) |\n| `retry_if_not_exception_message(match, regex=False)` | Retry if exception message does NOT contain the given string |\n| `retry_unless_exception_type(exc_types)` | Alias for `retry_if_not_exception_type` |\n\n```python\nfrom backon import retry_if_exception_type, retry_if_exception_message, retry_all\n\ncondition = retry_all(\n    retry_if_exception_type(HTTPError),\n    retry_if_exception_message(\"429\"),\n)\n```\n\n---\n\n## Jitter\n\n```python\n@backon.on_exception(backon.expo, ValueError, jitter=backon.full_jitter)\ndef f():\n    ...\n```\n\n| Jitter | Effect |\n|---|---|\n| `backon.full_jitter` | Random value between 0 and the calculated wait time |\n| `backon.random_jitter` | Adds `random()` to the calculated wait time (~+0.5s on average) |\n| `None` | No jitter (deterministic waits) |\n\n---\n\n## Handlers\n\nHandlers receive a `details` dict with contextual information:\n\n```python\ndef handler(details):\n    print(f\"Attempt {details['tries']}, elapsed {details['elapsed']:.2f}s\")\n\n@backon.on_exception(\n    backon.expo, ValueError, max_tries=3,\n    on_attempt=handler,\n    on_backoff=handler,\n    on_success=handler,\n    on_giveup=handler,\n)\ndef f():\n    ...\n```\n\nAvailable keys in `details`:\n\n| Key | Available in |\n|---|---|\n| `target` | All |\n| `args`, `kwargs` | All |\n| `tries` | All |\n| `elapsed` | All |\n| `value` | `on_success`, `on_backoff`, `on_giveup` |\n| `exception` | `on_backoff`, `on_giveup` |\n| `wait` | `on_backoff`, `before_sleep` |\n\n---\n\n## Global Toggle\n\nUseful in tests to disable retry logic globally:\n\n```python\nbackon.disable()   # skip retry, call function directly\nbackon.enable()    # re-enable retry\n```\n\nPer-instance toggle via `Retrying.enabled`:\n\n```python\nr = backon.Retrying(backon.expo, exception=ValueError, max_tries=3)\nr.enabled = False\nresult = r.call(fn)  # no retry\n```\n\n---\n\n## Generator Support\n\nSync and async generator functions are retried transparently. On each retry, the generator is restarted from scratch.\n\n```python\n@backon.on_exception(backon.expo, ValueError, max_tries=3)\ndef gen():\n    yield 1\n    yield 2\n    raise ValueError(\"fail\")\n    yield 3  # reached on retry\n\nresult = list(gen())  # [1, 2, 3]\n\n@backon.on_exception(backon.expo, ValueError, max_tries=3)\nasync def agen():\n    yield 1\n    raise ValueError(\"fail\")\n    yield 2\n\nresult = [item async for item in agen()]  # [1, 2]\n```\n\n`retry_with()` works on generators too:\n\n```python\nwrapped = gen.retry_with(max_tries=5)\n```\n\n---\n\n## Async Support\n\nAll three APIs work with async functions transparently:\n\n```python\n@backon.on_exception(backon.expo, ValueError, max_tries=3)\nasync def fetch():\n    return await api.call()\n\nresult = await backon.retry(fetch, backon.expo, exception=ValueError, max_tries=3)\n\nasync with backon.Retrying(backon.expo, exception=ValueError, max_tries=3) as r:\n    result = await r.async_call(fetch)\n```\n\n---\n\n## Custom Sleep\n\nReplace the default sleep for testing or special environments:\n\n```python\n@backon.on_exception(\n    backon.expo, ValueError, max_tries=3,\n    sleep=lambda s: print(f\"waiting {s}s\"),\n)\ndef f():\n    ...\n\n# With asyncio.Event for testing\nimport asyncio\n\nevent = asyncio.Event()\n@backon.on_exception(\n    backon.expo, ValueError, max_tries=3,\n    sleep=backon.sleep_using_event(event),\n)\nasync def f():\n    ...\n```\n\n---\n\n## Advanced Features\n\n### Rate Limiter\n\nThrottle retry calls to avoid overwhelming a backend.\n\n```python\nfrom backon import RateLimiter\n\nlimiter = RateLimiter(max_calls=10, period=1.0)  # max 10 calls per second\n\n@backon.on_exception(backon.expo, ValueError, max_tries=5, rate_limit=limiter)\ndef fetch():\n    ...\n```\n\n`RateLimitError` is raised only by direct `RateLimiter.__call__` / `RateLimiter.acquire` usage. Inside a retry loop (`rate_limit=...`), exceeding the limit just throttles — the loop sleeps for the computed backoff before the next attempt, and `RateLimitError` is never raised.\n\n### TryAgain\n\nRaise `TryAgain` inside a retried function to force an immediate retry, bypassing any condition or stop logic:\n\n```python\nimport backon\nfrom backon import TryAgain\n\n@backon.on_exception(backon.expo, ValueError, max_tries=3)\ndef fetch():\n    try:\n        return api.call()\n    except TemporaryIssue:\n        raise TryAgain()\n```\n\n### Circuit Breaker\n\nCircuit breaker with three states: CLOSED (normal), OPEN (failing), HALF_OPEN (testing recovery).\n\n```python\nfrom backon import CircuitBreaker, BreakerRetrying, CircuitOpenError\n\nbreaker = BreakerRetrying(\n    backon.expo, max_tries=3,\n    breaker=CircuitBreaker(\n        failure_threshold=5,\n        recovery_timeout=60.0,\n        half_open_max_calls=1,\n    ),\n)\n\ntry:\n    result = breaker.call(fetch)\nexcept CircuitOpenError:\n    print(\"Circuit is open, skipping request\")\n```\n\n| `CircuitBreaker` parameter | Default | Description |\n|---|---|---|\n| `failure_threshold` | `5` | Consecutive failures before opening the circuit |\n| `recovery_timeout` | `60.0` | Seconds before transitioning from OPEN to HALF_OPEN |\n| `half_open_max_calls` | `1` | Allowed calls in HALF_OPEN state before fully closing |\n| `name` | `\"\"` | Identifier for the breaker |\n\n### Hedging\n\nRun multiple retry attempts concurrently and return the first success.\n\n```python\nfrom backon import hedge, HedgingRetrying, on_hedge\n\n# Functional\nresult = hedge(fetch, backon.expo, max_hedge=3)\n\n# Decorator\n@on_hedge(backon.expo, max_hedge=3)\ndef fetch():\n    ...\n\n# Context manager\nwith HedgingRetrying(backon.expo, max_hedge=3) as h:\n    result = h.call(fetch)\n```\n\n| Parameter | Default | Description |\n|---|---|---|\n| `max_hedge` | `3` | Number of concurrent hedged requests |\n| `timeout` | `None` | Maximum time to wait for any hedge |\n| `on_hedge` | `None` | Callback when a hedge request is sent |\n\n### Testing Utilities\n\n```python\nfrom backon import (\n    disable_retries, enable_retries,\n    test_config, limit_retries, remove_backoff,\n    assert_retried, assert_not_retried,\n)\n\n# Context manager that skips retry for a block\nwith disable_retries():\n    result = fetch()\n\n# Limit max retries in tests\nwith limit_retries(2):\n    fetch()\n\n# Remove backoff delay entirely\nwith remove_backoff():\n    fetch()\n\n# Assert the function was retried N times\nassert_retried(fetch, expected_tries=3)\n```\n\n### Trio Support\n\nRetry with the trio async framework. The trio helpers live in the `backon._trio` module: the `trio` package is an optional dependency, so the trio helpers are deliberately not part of the top-level `backon` namespace and must be imported explicitly.\n\n```python\nfrom backon._trio import retry_exception, retry_predicate\n\n@retry_exception(backon.expo, ValueError, max_tries=3)\nasync def fetch():\n    ...\n```\n\nRequires `trio` to be installed.\n\n### Retry Context Inspection\n\nCheck if code is running inside a retry and get the current attempt number anywhere in the call stack:\n\n```python\nfrom backon import is_retrying, get_attempt_number\n\ndef log_attempt():\n    if is_retrying():\n        print(f\"This is attempt #{get_attempt_number()}\")\n\n@backon.on_exception(backon.expo, ValueError, max_tries=3)\ndef fetch():\n    log_attempt()\n    return api.call()\n```\n\nUses `contextvars` — thread-safe and async-safe.\n\n### Dynamic Backoff\n\nOverride the wait time per attempt by returning a `float` from the `giveup` callback. Useful for respecting `Retry-After` headers.\n\n```python\ndef respect_retry_after(exc: HTTPError) -\u003e float:\n    return exc.response.headers.get(\"Retry-After\", 1.0)\n\n@backon.on_exception(backon.expo, HTTPError, giveup=respect_retry_after)\ndef fetch():\n    ...\n```\n\n### Hot Loop Detection\n\nWhen 5 or more retries occur with less than 100ms between them, backon logs a warning. This helps detect misconfigured retry policies before they cause issues.\n\n### Retry Statistics\n\n```python\nr = backon.Retrying(backon.expo, exception=ValueError, max_tries=3)\nresult = r.call(fetch)\n\nprint(r.statistics)\n# {'start_time': ..., 'attempt_number': 2, 'idle_for': 1.5, 'elapsed': 2.3}\n\nprint(r.call_state)\n# RetryCallState(fn=..., attempt_number=2, ...)\n```\n\n### Operator Composition\n\nCompose stops, conditions, and wait generators using Python operators:\n\n```python\n# Stop when either condition is met\nstop = stop_after_attempt(5) | stop_after_delay(30.0)\n\n# Retry when both conditions pass\ncond = retry_if_exception_type(TimeoutError) \u0026 retry_if_result(lambda x: x is None)\n\n# Wait with combined strategy\nwait = backon.expo(base=3) + backon.constant(interval=0.5)\n```\n\n### Iterator API\n\n```python\nfor attempt in backon.Retrying(backon.expo, exception=ValueError, max_tries=3):\n    with attempt:\n        result = fetch()\n    if not attempt.failed:\n        break\n```\n\n---\n\n## Migrating from backoff\n\nbackon is a near-drop-in replacement. Change your imports:\n\n```diff\n- import backoff\n+ import backon\n\n- @backoff.on_exception(backoff.expo, ValueError, max_tries=3)\n+ @backon.on_exception(backon.expo, ValueError, max_tries=3)\n```\n\nNote: `backoff.__version__` was a static string. `backon.__version__` is read from `importlib.metadata` (so `pip install backon` is required for the real value); running only from a source checkout returns `\"0.0.0\"`. (Use `pip install -e .` for editable installs.)\n\nKey differences:\n\n| Area | backoff | backon |\n|---|---|---|\n| Python support | 3.7+ | 3.10+ |\n| Type hints | Partial | Full |\n| `on_attempt` callback | Not supported | Supported |\n| Context manager | Not supported | `Retrying` class |\n| Functional API | Not supported | `retry()` function, `RetryingCaller` |\n| Global toggle | Not supported | `disable()` / `enable()` |\n| Custom sleep | Not supported | `sleep=` parameter |\n| Circuit breaker | Not supported | `CircuitBreaker` + `BreakerRetrying` |\n| Hedging | Not supported | `hedge()` / `on_hedge()` |\n| Metrics | Not supported | Prometheus / OTel |\n| Wait generator composition | Not supported | `+` operator |\n| Stop / RetryCondition composition | Not supported | `\\|` / `\u0026` operators |\n| Trio | Not supported | `backon._trio` (deliberately private, optional dep) |\n| Iterator API | Not supported | `for attempt in Retrying():` |\n| Build system | Poetry | PDM (PEP 621) |\n\n---\n\n## Contributing\n\n```bash\ngit clone https://github.com/Llucs/backon.git\ncd backon\npip install pdm\npdm install\npdm run ruff check backon/ tests/\npdm run mypy backon/\npdm run pytest tests/ -q\n```\n\n---\n\n## Contributors\n\n\u003ca href=\"https://github.com/Llucs/backon/graphs/contributors\"\u003e\n  \u003cimg src=\"https://contrib.rocks/image?repo=Llucs/backon\" alt=\"Contributors\" /\u003e\n\u003c/a\u003e\n\n---\n\n## License\n\n[MIT](https://github.com/Llucs/backon/blob/main/LICENSE)\n\nMade by Llucs with ❤️\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fllucs%2Fbackon","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fllucs%2Fbackon","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fllucs%2Fbackon/lists"}