A composable retry policy framework with classification, jitter strategies, and observability hooks.
Project description
redress
redress (v.): to remedy or to set right.
Classifier-driven retries with per-class backoff and structured hooks for Python services.
Composable, low-overhead retry policies with sync/async symmetry, deterministic envelopes, and lightweight composition.
Designed for services that need predictable retry behavior and clean integration with metrics/logging.
Documentation
- Site: https://aponysus.github.io/redress/
- Getting started: https://aponysus.github.io/redress/getting-started/
Installation
From PyPI:
uv pip install redress
# or
pip install redress
Quick Start
from redress.policy import RetryPolicy
from redress.classify import default_classifier
from redress.strategies import decorrelated_jitter
policy = RetryPolicy(
classifier=default_classifier,
strategy=decorrelated_jitter(max_s=10.0),
)
def flaky():
# your operation that may fail
...
result = policy.call(flaky)
Decorator quick start
from redress import retry, default_classifier
from redress.strategies import decorrelated_jitter
@retry # defaults to default_classifier + decorrelated_jitter(max_s=5.0)
def fetch_user():
...
# Or customize classifier/strategies
@retry(
classifier=default_classifier,
strategy=decorrelated_jitter(max_s=3.0),
)
def fetch_user_custom():
...
If you provide `strategies` without `strategy`, the decorator will not add a default
strategy.
# Context manager for repeated calls with shared hooks/operation
policy = RetryPolicy(classifier=default_classifier, strategy=decorrelated_jitter(max_s=3.0))
with policy.context(operation="batch") as retry:
retry(fetch_user)
Async quick start
import asyncio
from redress import AsyncRetryPolicy, default_classifier
from redress.strategies import decorrelated_jitter
async_policy = AsyncRetryPolicy(
classifier=default_classifier,
strategy=decorrelated_jitter(max_s=5.0),
)
async def flaky_async():
...
asyncio.run(async_policy.call(flaky_async))
Why redress?
Most retry libraries give you either:
- decorators with a fixed backoff model, or
- one global strategy for all errors.
redress gives you something different:
✔ Exception → coarse error class mapping
Provided via default_classifier.
✔ Per-class strategy dispatch
Each ErrorClass can use its own backoff logic.
✔ Dependency-free strategies with jitter
decorrelated_jitter, equal_jitter, token_backoff.
✔ Deadlines, max attempts, and separate caps for UNKNOWN
Deterministic retry envelopes.
✔ Clean observability hook
Single callback for:
success, retry, permanent_fail, deadline_exceeded, max_attempts_exceeded, max_unknown_attempts_exceeded.
Error Classes & Classification
PERMANENT
CONCURRENCY
RATE_LIMIT
SERVER_ERROR
TRANSIENT
UNKNOWN
Redress intentionally keeps ErrorClass small and fixed. The goal is semantic
classification ("rate limit" vs. "server error") rather than mechanical mapping to
every exception type. If you need finer-grained behavior, use separate policies per
use case. Optional classification context can carry hints (for example, Retry-After)
without expanding the class set.
Classification rules:
- Explicit redress error types
- Numeric codes (
err.statusorerr.code) - Name heuristics
- Fallback to UNKNOWN
Name heuristics are a convenience for quick starts; for production, prefer a domain-specific
classifier (HTTP/DB/etc.) or strict_classifier to avoid surprises.
Classifiers can return Classification(klass=..., retry_after_s=..., details=...) to pass
structured hints to strategies. Returning ErrorClass is shorthand for
Classification(klass=klass).
Metrics & Observability
def metric_hook(event, attempt, sleep_s, tags):
print(event, attempt, sleep_s, tags)
policy.call(my_op, on_metric=metric_hook)
Backoff Strategies
Strategy signature (context-aware):
(ctx: BackoffContext) -> float
Legacy signature (still supported):
(attempt: int, klass: ErrorClass, prev_sleep: Optional[float]) -> float
Built‑ins:
decorrelated_jitter()equal_jitter()token_backoff()retry_after_or(...)
Per-Class Example
policy = RetryPolicy(
classifier=default_classifier,
strategy=decorrelated_jitter(max_s=10.0), # default
strategies={
ErrorClass.CONCURRENCY: decorrelated_jitter(max_s=1.0),
ErrorClass.RATE_LIMIT: decorrelated_jitter(max_s=60.0),
ErrorClass.SERVER_ERROR: equal_jitter(max_s=30.0),
},
)
Deadline & Attempt Controls
policy = RetryPolicy(
classifier=default_classifier,
strategy=decorrelated_jitter(),
deadline_s=60,
max_attempts=8,
max_unknown_attempts=2,
)
Development
uv run pytest
CLI
- Lint a retry config or policy to catch obvious misconfigurations:
# app_retry.py
from redress import RetryConfig
from redress.strategies import decorrelated_jitter
cfg = RetryConfig(
default_strategy=decorrelated_jitter(max_s=1.5),
max_attempts=5,
)
Then from the repo root or any env where app_retry is on PYTHONPATH:
redress doctor app_retry:cfg
# Show a normalized snapshot of active values:
redress doctor app_retry:cfg --show
doctor accepts module:attribute pointing to a RetryConfig, RetryPolicy, or AsyncRetryPolicy. The attribute defaults to config if omitted (e.g., myapp.settings will look for settings:config).
Example --show output:
Config snapshot:
source: app_retry:cfg
deadline_s: 60.0
max_attempts: 5
max_unknown_attempts: 2
default_strategy: redress.strategies.decorrelated_jitter.<locals>.f
class_strategies:
(none)
per_class_max_attempts:
(none)
OK: 'app_retry:cfg' passed config checks.
Examples (in docs/snippets/)
- Sync httpx demo:
uv pip install httpxthenuv run python docs/snippets/httpx_sync_retry.py - Async httpx demo using
AsyncRetryPolicy:uv pip install httpxthenuv run python docs/snippets/httpx_async_retry.py - Async worker loop with retries:
uv run python docs/snippets/async_worker_retry.py - Decorator usage (sync + async):
uv run python docs/snippets/decorator_retry.py - FastAPI proxy with metrics counter:
uv pip install "fastapi[standard]" httpxthenuv run uvicorn docs.snippets.fastapi_downstream:app --reload - FastAPI middleware with per-endpoint policies:
uv pip install "fastapi[standard]" httpxthenuv run uvicorn docs.snippets.fastapi_middleware:app --reload - PyODBC + SQLSTATE classification example:
uv pip install pyodbcthenuv run python docs/snippets/pyodbc_retry.py - requests example:
uv pip install requeststhenuv run python docs/snippets/requests_retry.py - asyncpg example:
uv pip install asyncpgand setASYNC_PG_DSN, thenuv run python docs/snippets/asyncpg_retry.py - Pyperf microbenchmarks:
uv pip install .[dev]thenuv run python docs/snippets/bench_retry.py
Docs site
- Build/serve locally:
uv pip install .[docs]thenuv run mkdocs serve - Pages:
docs/index.md,docs/usage.md,docs/observability.md,docs/recipes.mdwith runnable snippets indocs/snippets/.
Versioning
Semantic Versioning.
Project details
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 redress-1.1.0.tar.gz.
File metadata
- Download URL: redress-1.1.0.tar.gz
- Upload date:
- Size: 132.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0ec072f56192b775324d53ccbe3bcccf68cddd927d847016d00da1d151e2e0c7
|
|
| MD5 |
cf0a126968d21de40cd627e20487deaf
|
|
| BLAKE2b-256 |
6322d4d23af42d0e4ea18e03badc95c9e4bb62133477c6f85586b92390a48b33
|
File details
Details for the file redress-1.1.0-py3-none-any.whl.
File metadata
- Download URL: redress-1.1.0-py3-none-any.whl
- Upload date:
- Size: 47.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b7c1490e1189a7fdf2fb21eae29dc60c89801f72b8ae93aa85500c4db5314377
|
|
| MD5 |
5f3f7395b8a4f529cbec761676b37d79
|
|
| BLAKE2b-256 |
fb12030e3dd5b9b222931d2852c774e1e56048d61a4f06bf930fcf496a69071f
|