Smart retry engine with exponential backoff, jitter, per-exception rules, timeout, and async support
Project description
retryflow
Smart retry engine for Python — exponential backoff, jitter, per-exception rules, per-attempt timeout, hooks, and full async support.
Why retryflow?
Every app that talks to a network, a database, or an external API will eventually hit a transient failure. retryflow gives you a battle-tested, zero-dependency retry engine that handles the hard parts:
- Exponential backoff so you don't hammer a struggling service
- Jitter to avoid thundering-herd problems across multiple clients
- Per-attempt timeouts so a single hung call can't block forever
- Per-exception rules so you only retry errors that make sense to retry
- Lifecycle hooks (
on_retry,on_success,on_failure) for logging, alerting, metrics - Native async support — works seamlessly with
async deffunctions - Context manager API for one-off retry blocks without decorating a function
Installation
pip install retryflow
No dependencies. Requires Python 3.8+.
Quick Start
from retryflow import retry
@retry(max_attempts=3, delay=1.0, backoff=2.0)
def fetch_data(url):
response = requests.get(url, timeout=5)
response.raise_for_status()
return response.json()
That's it. On failure, retryflow waits 1s, then 2s, then raises RetryError.
Usage
1. Basic Decorator
from retryflow import retry
@retry(max_attempts=5, delay=0.5, backoff=2.0, jitter=0.1)
def call_api():
...
Also works with no arguments (uses defaults):
@retry
def call_api():
...
2. Per-Attempt Timeout
Stop a single attempt if it hangs beyond a time limit:
@retry(max_attempts=3, delay=1.0, timeout=5.0)
def slow_query():
# If this takes more than 5s, a TimeoutError is raised
# and retryflow retries it
return db.execute(heavy_query)
3. Retry Only Specific Exceptions
@retry(
max_attempts=5,
delay=1.0,
exceptions=(ConnectionError, TimeoutError) # Only retry these
)
def connect():
...
Non-matching exceptions (e.g., ValueError) propagate immediately without retrying.
4. Lifecycle Hooks
from retryflow import retry, RetryContext
def on_retry(ctx: RetryContext):
print(f"Attempt {ctx.attempt}/{ctx.max_attempts} failed: {ctx.exception}")
print(f"Retrying in {ctx.next_wait:.2f}s...")
def on_failure(ctx: RetryContext):
alert_team(f"{ctx.func_name} failed after {ctx.attempts} attempts")
def on_success(ctx: RetryContext):
metrics.record("api.success", tags={"attempt": ctx.attempt})
@retry(
max_attempts=5,
delay=1.0,
on_retry=on_retry,
on_failure=on_failure,
on_success=on_success,
)
def critical_call():
...
5. Async Functions
Works exactly the same — retryflow detects async def automatically:
@retry(max_attempts=4, delay=0.5, timeout=3.0)
async def async_fetch(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
return await resp.json()
6. Reusable Config Object
from retryflow import retry, RetryConfig
# Define once, apply everywhere
production_retry = RetryConfig(
max_attempts=5,
delay=1.0,
backoff=2.0,
jitter=0.3,
timeout=10.0,
exceptions=(ConnectionError, TimeoutError),
log_retries=True,
)
@retry(config=production_retry)
def service_a(): ...
@retry(config=production_retry)
def service_b(): ...
7. Context Manager
For one-off retries without decorating a function:
from retryflow import attempt
with attempt(max_attempts=3, delay=1.0, timeout=5.0) as r:
data = r.run(fetch, "https://api.example.com/data")
print(f"Completed in {r.elapsed:.2f}s")
API Reference
@retry decorator
retry(
func=None, # The function to wrap (when used without parentheses)
*,
max_attempts=3, # Total attempts including the first call
delay=1.0, # Base delay in seconds between attempts
backoff=2.0, # Delay multiplier (1.0=constant, 2.0=exponential)
jitter=0.0, # Random seconds added to each wait (0 to jitter)
timeout=None, # Max seconds per attempt (None = no limit)
exceptions=(Exception,),# Only retry on these exception types
on_retry=None, # Callable(RetryContext) called before each retry
on_failure=None, # Callable(RetryContext) called when all attempts fail
on_success=None, # Callable(RetryContext) called on success
log_retries=True, # Emit warning logs on each retry
config=None, # RetryConfig object (overrides all other params)
)
RetryConfig
All the same parameters as @retry, packaged into a reusable object.
cfg = RetryConfig(max_attempts=5, delay=1.0, backoff=2.0, timeout=10.0)
RetryContext
Passed to hook callbacks with information about the current state.
| Attribute | Type | Description |
|---|---|---|
attempt |
int |
Current attempt number (1-indexed) |
max_attempts |
int |
Total configured attempts |
exception |
Exception |
The exception that just occurred |
elapsed |
float |
Total elapsed seconds since first attempt |
next_wait |
float | None |
Seconds until next retry (None on final attempt) |
func_name |
str |
Name of the decorated function |
RetryError
Raised when all attempts are exhausted.
| Attribute | Type | Description |
|---|---|---|
last_exception |
Exception |
The final exception raised |
attempts |
int |
Total number of attempts made |
attempt context manager
with attempt(max_attempts=3, delay=1.0, ...) as r:
result = r.run(func, *args, **kwargs)
After the with block, r.result, r.elapsed, r.last_error, and r.total_attempts are available.
Backoff Formula
wait = delay * (backoff ^ (attempt - 1)) + random(0, jitter)
| attempt | delay=1, backoff=2 | delay=0.5, backoff=3 |
|---|---|---|
| 1st retry | 1.0s | 0.5s |
| 2nd retry | 2.0s | 1.5s |
| 3rd retry | 4.0s | 4.5s |
Running Tests
pip install pytest pytest-asyncio
pytest tests/ -v
License
MIT © prabhay759
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file retryflow-1.0.0.tar.gz.
File metadata
- Download URL: retryflow-1.0.0.tar.gz
- Upload date:
- Size: 11.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
699f2c5b0ee363a60c0714a2dbfa92af742bd4051d073c86bee81fa3dedb08ce
|
|
| MD5 |
cea4bebbf9935ab6f4caea282fa6eb8a
|
|
| BLAKE2b-256 |
211838501e637f26845801d94cb533cd63851379398292292c2ba9f8d6134d65
|
Provenance
The following attestation bundles were made for retryflow-1.0.0.tar.gz:
Publisher:
publish.yml on prabhay759/retryflow
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
retryflow-1.0.0.tar.gz -
Subject digest:
699f2c5b0ee363a60c0714a2dbfa92af742bd4051d073c86bee81fa3dedb08ce - Sigstore transparency entry: 1216494583
- Sigstore integration time:
-
Permalink:
prabhay759/retryflow@226d0591c079dfe3195ffb8824e77f2dd29b725c -
Branch / Tag:
refs/heads/main - Owner: https://github.com/prabhay759
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@226d0591c079dfe3195ffb8824e77f2dd29b725c -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file retryflow-1.0.0-py3-none-any.whl.
File metadata
- Download URL: retryflow-1.0.0-py3-none-any.whl
- Upload date:
- Size: 8.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0d1a2156069656dafba164a552b3b8dd39d279980a298d5c02d81e8890ab8483
|
|
| MD5 |
1265787cd0356e0f6bce8ea1afdbe5c7
|
|
| BLAKE2b-256 |
9fe9539411ba3cdda6bffa976fb325055f7643b43bd5f8bb78db500e6b688af4
|
Provenance
The following attestation bundles were made for retryflow-1.0.0-py3-none-any.whl:
Publisher:
publish.yml on prabhay759/retryflow
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
retryflow-1.0.0-py3-none-any.whl -
Subject digest:
0d1a2156069656dafba164a552b3b8dd39d279980a298d5c02d81e8890ab8483 - Sigstore transparency entry: 1216494677
- Sigstore integration time:
-
Permalink:
prabhay759/retryflow@226d0591c079dfe3195ffb8824e77f2dd29b725c -
Branch / Tag:
refs/heads/main - Owner: https://github.com/prabhay759
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@226d0591c079dfe3195ffb8824e77f2dd29b725c -
Trigger Event:
workflow_dispatch
-
Statement type: