Skip to main content

A composable retry policy framework with classification, jitter strategies, and observability hooks.

Project description

redress

CI codecov PyPI Version Docs Bench

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

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.status or err.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 httpx then uv run python docs/snippets/httpx_sync_retry.py
  • Async httpx demo using AsyncRetryPolicy: uv pip install httpx then uv 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]" httpx then uv run uvicorn docs.snippets.fastapi_downstream:app --reload
  • FastAPI middleware with per-endpoint policies: uv pip install "fastapi[standard]" httpx then uv run uvicorn docs.snippets.fastapi_middleware:app --reload
  • PyODBC + SQLSTATE classification example: uv pip install pyodbc then uv run python docs/snippets/pyodbc_retry.py
  • requests example: uv pip install requests then uv run python docs/snippets/requests_retry.py
  • asyncpg example: uv pip install asyncpg and set ASYNC_PG_DSN, then uv run python docs/snippets/asyncpg_retry.py
  • Pyperf microbenchmarks: uv pip install .[dev] then uv run python docs/snippets/bench_retry.py

Docs site

  • Build/serve locally: uv pip install .[docs] then uv run mkdocs serve
  • Pages: docs/index.md, docs/usage.md, docs/observability.md, docs/recipes.md with runnable snippets in docs/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

redress-1.1.0.tar.gz (132.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

redress-1.1.0-py3-none-any.whl (47.3 kB view details)

Uploaded Python 3

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

Hashes for redress-1.1.0.tar.gz
Algorithm Hash digest
SHA256 0ec072f56192b775324d53ccbe3bcccf68cddd927d847016d00da1d151e2e0c7
MD5 cf0a126968d21de40cd627e20487deaf
BLAKE2b-256 6322d4d23af42d0e4ea18e03badc95c9e4bb62133477c6f85586b92390a48b33

See more details on using hashes here.

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

Hashes for redress-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b7c1490e1189a7fdf2fb21eae29dc60c89801f72b8ae93aa85500c4db5314377
MD5 5f3f7395b8a4f529cbec761676b37d79
BLAKE2b-256 fb12030e3dd5b9b222931d2852c774e1e56048d61a4f06bf930fcf496a69071f

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page