Migration
Use this page when you are evaluating the cost of moving from retry-only libraries to redress.
What changes when you move to redress
The main difference is abstraction level.
Most retry libraries focus on:
- retry predicates
- decorator ergonomics
- backoff behavior
redress adds:
- explicit semantic classification (
ErrorClass) - a canonical policy container
- structured stop reasons and outcomes
- shared retry budgets
- circuit breakers
- low-cardinality observability hooks
That means migration usually makes a few things more explicit, but gives you a stronger model once the code stops being “just a decorator.”
From Tenacity-style decorators
Typical pattern:
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(stop=stop_after_attempt(5), wait=wait_exponential())
def fetch_user():
...
Nearest redress equivalent:
from redress import retry, default_classifier
from redress.strategies import decorrelated_jitter
@retry(
classifier=default_classifier,
strategy=decorrelated_jitter(max_s=5.0),
max_attempts=5,
)
def fetch_user():
...
What becomes more explicit:
- how failures are classified
- what kinds of errors should be treated differently
What you gain:
- per-class behavior
- stable stop reasons
- a clean path to
Policy(...), budgets, and breakers
From Backoff-style decorators
Typical pattern:
import backoff
@backoff.on_exception(backoff.expo, Exception, max_tries=5)
def fetch_user():
...
Nearest redress equivalent:
from redress import retry, default_classifier
from redress.strategies import decorrelated_jitter
@retry(
classifier=default_classifier,
strategy=decorrelated_jitter(max_s=5.0),
max_attempts=5,
)
def fetch_user():
...
What changes:
- retryability is classification-driven instead of “retry this broad exception set”
- you can move naturally toward explicit policy objects as behavior grows
When to stop using decorator-only migration
Move from the decorator to Policy when you need:
- circuit breakers
- shared retry budgets
- result-based retries
execute()/RetryOutcome- multiple operations sharing one configuration
Retry-only object migration
If you currently think in terms of a retry object rather than a decorator, the closest shape is:
from redress import RetryPolicy, default_classifier
from redress.strategies import decorrelated_jitter
policy = RetryPolicy(
classifier=default_classifier,
strategy=decorrelated_jitter(max_s=5.0),
max_attempts=5,
deadline_s=30.0,
)
Then, when you need the wider execution model:
from redress import Policy, Retry, default_classifier
from redress.strategies import decorrelated_jitter
policy = Policy(
retry=Retry(
classifier=default_classifier,
strategy=decorrelated_jitter(max_s=5.0),
max_attempts=5,
deadline_s=30.0,
),
)
HTTP 429 / Retry-After migration
This is a common place where redress gets more valuable than a generic retry decorator.
from redress import RetryPolicy
from redress.extras import http_retry_after_classifier
from redress.strategies import decorrelated_jitter, retry_after_or
policy = RetryPolicy(
classifier=http_retry_after_classifier,
strategy=retry_after_or(decorrelated_jitter(max_s=10.0)),
max_attempts=5,
)
This keeps rate limiting separate from generic transient failure handling.
What redress makes more explicit on purpose
- failures should map to coarse semantic classes
- retries should be bounded
- observability should use stable, low-cardinality fields
- callers should be able to ask why retries stopped
If that explicitness feels heavy, the decorator may be enough. If the code is already carrying ad-hoc retry rules, redress usually makes the behavior easier to reason about.