# stamina: Opinionated Retries for Python Services

> stamina adds bounded retries, jittered backoff, async support, instrumentation, and testing controls around Tenacity. It is designed for Python services that must distinguish repeatable transient failures from errors that should return immediately.

**hynek/stamina** — Production-grade retries for Python

- Repository: https://github.com/hynek/stamina
- Website: https://stamina.hynek.me/
- Stars: 1,462 · Forks: 26
- Language: Python
- License: MIT
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/hynek-stamina

## Why stamina Exists Beside Tenacity

stamina is aimed at a recurring problem in distributed Python systems: an operation can fail transiently, but retrying every failure can add load, delay error reporting, and duplicate work. The library gives application developers a compact policy for retrying selected operations such as HTTP requests, database calls, and service-to-service requests. It is meant to reduce the amount of Tenacity configuration repeated at each call site.

The README lists several defaults and controls. Retries can be restricted to selected exception classes, or a backoff hook can inspect an exception before deciding whether to retry it. Retries use exponential backoff with jitter, and both the number of attempts and the total time are limited. These controls address separate risks: broad exception matching wastes attempts, backoff controls request frequency, and time limits prevent an individual operation from consuming an unbounded delay budget. Jitter also reduces synchronized retry waves when several clients fail against the same dependency.

stamina is deliberately opinionated. It does not present itself as a fully configurable replacement for Tenacity. That is useful for teams that want one documented retry policy across many call sites, but it may be restrictive for applications that need custom scheduling, custom retry predicates, or unusual execution controls. The repository requires Python `>=3.10`, so compatibility is another adoption boundary.

## How stamina Wraps Tenacity

The `pyproject.toml` declares `tenacity>=8.3.0` as the runtime dependency. Based on the repository layout, stamina's implementation is under `src/`, with tests under `tests/` and typing checks under `typing_tests/`. The README describes stamina as an opinionated wrapper around Tenacity, so teams should understand that the underlying retry engine remains part of the dependency chain.

The documented decorator applies a retry policy to a callable. The `on=` argument identifies the exception types that qualify for a retry, while `attempts=` limits repeated work. The README says type hints from the decorated callable are preserved, which makes the wrapper more suitable for typed Python code than a decorator that erases the callable's static signature.

Async callables use the same API, and the README also says that arbitrary blocks can be retried. The latter changes the retry boundary from a function declaration to a selected region of code. That can prevent unrelated work from being repeated, but the developer must place state changes, cleanup, and side effects outside the boundary when they must not run again. The README does not give the block API's exact syntax, so the project tutorial is the appropriate source for its implementation details.

The project documents instrumentation for Prometheus, structlog, and the standard library's `logging` module. It does not specify metric names, event fields, or whether activation uses decorators, context managers, or another configuration interface in the README. Teams that need a fixed telemetry schema should inspect the corresponding documentation before connecting dashboards or alerts.

## Retry Boundaries for a Python HTTP Call

The README links to PyPI for distribution and directs readers to the documentation tutorial for more examples. It does not show an installation command. The `pyproject.toml` establishes the package metadata: Python `>=3.10`, the `stamina` package, and the `tenacity>=8.3.0` dependency.

The README provides a concrete synchronous HTTP example using `httpx2`. The decorator selects `httpx2.HTTPError` and sets `attempts=3`. The function calls `httpx2.get()`, invokes `resp.raise_for_status()`, and returns the response. A status error therefore enters the retry policy through `raise_for_status()`.

```python
import httpx2
import stamina

@stamina.retry(on=httpx2.HTTPError, attempts=3)
def do_it(code: int) -> httpx2.Response:
    resp = httpx2.get(f"https://httpbin.org/status/{code}")
    resp.raise_for_status()
    return resp
```

A call such as `do_it(500)` requests a server error. Whether the initial invocation is included in the three-attempt count is not specified in the README. The example also does not configure a total-time limit. Developers who need exact timing semantics should consult the API documentation rather than infer them from the decorator example.

The boundary remains application-specific. Retrying a GET request can be appropriate when the server response is transient and the operation has no local side effects. Repeating a write requires an idempotency key, a deduplication mechanism, or another explicit safety decision. stamina can retry a selected exception, but the README does not claim that it detects whether repeated execution is safe.

## Async Blocks, Filtering, and Instrumentation

stamina supports async callables and Trio, according to the README. The project metadata also lists asyncio and Trio as supported frameworks. This makes it possible to keep retry policies similar across synchronous and asynchronous code, although the actual execution environment still determines how many requests can execute concurrently.

Exception filtering is the first line of control. A narrow exception class avoids spending attempts on errors that will not become valid later. The backoff hook goes further by inspecting the exception itself, which can distinguish a subset of failures. That flexibility also creates a testing requirement: hook decisions should be covered for retryable and non-retryable cases. The README does not specify the hook's full interface, so its return contract must come from the API documentation.

The Prometheus, structlog, and `logging` integrations address different observability environments. The README confirms that these integrations exist, but it does not state whether they emit separate metrics for retries, exhausted retries, or final failures. Nor does it document whether exception classes are included in event fields. An application with an existing logging schema should verify the emitted records before depending on them for alerts.

The arbitrary-block support should be used carefully. If a block includes a database write followed by a cache update, retrying the entire block may repeat the write even when the cache update failed. Smaller retry regions can limit repetition, but they can also leave partial state unless the surrounding code provides compensation. stamina supplies the retry mechanism, not transactional semantics.

## Testing Retries Without Production Delays

The README describes dedicated testing controls. Retries can be globally deactivated, limited to a smaller number of attempts, or stripped of backoffs. These controls prevent unit tests from waiting through production delays while still allowing test suites to cover retry-related paths.

Global deactivation is useful for tests focused on the wrapped operation's result. Attempt limiting is more appropriate when a test needs to verify that a failure was retried. Removing backoff keeps those tests fast, although it changes timing and should not be used to infer production latency.

The README does not document the exact testing API, scope, nesting behavior, or concurrency guarantees. That matters because a global switch may affect every decorated call in a process, including code exercised by a shared fixture. Tests that need independent retry policies should verify the documented scope before relying on a global setting.

Failure-path tests should also confirm that permanent exceptions leave the retry boundary immediately. An HTTP test should verify how status errors are converted into exceptions and whether the final result is returned or raised after the configured attempts. The library can enforce the configured filter and limit, but the application must define which responses are genuinely transient and which operation is safe to repeat.

## Operational Limits, License, and Upgrade Cost

The main operational limitation is that retry policy and side-effect safety are separate concerns. stamina can bound attempts and filter exceptions, but the README does not claim automatic idempotency detection, circuit breaking, distributed coordination, or global protection from retry storms. A generous attempt count can still be wrong for a non-idempotent write or for a dependency that is unavailable for an extended period.

The compatibility declaration requires Python 3.10 or later and lists Python 3.10 through 3.15, asyncio, and Trio. The README does not provide a migration guide or upgrade procedure. Before upgrading, review `CHANGELOG.md`, especially if the application depends on defaults for attempts, backoff, exception filtering, or instrumentation.

The repository is not archived, and the last push was on 2026-10-16. The listed releases are 26.1.0 on 2026-04-13, 25.2.0 on 2025-12-11, and 25.1.0 on 2025-03-12. The README does not state a per-version support commitment, so release notes and changelog entries are the concrete upgrade checks available to users.

The project uses the MIT License. The README credits Hynek Schlawack as the author and identifies Tenacity as the upstream project. The full `LICENSE` file contains the terms and notices for stamina. Teams should preserve those notices and separately review the terms of every dependency they redistribute. This paragraph describes the declared licensing information and is not legal advice.

## Conclusion

Adopt stamina for service code performing idempotent or carefully controlled operations against networks and other transient dependencies. Do not use it to repeat permanent failures or non-idempotent mutations without separate safeguards. Before adoption, verify Python 3.10 or later, define the exception policy for each operation, and check how the testing switches affect the call sites.

## FAQ

### What problem does stamina solve in Python?

It provides an opinionated wrapper around Tenacity for retrying selected transient failures with exponential backoff, jitter, attempt limits, and total-time limits. It is intended for Python service code that performs repeatable network or I/O operations.

### Does stamina support async Python and Trio?

Yes. The README states that async callables use the same API and specifically mentions automatic async support including Trio. The repository metadata also lists asyncio and Trio as supported frameworks.

### How can I test code that uses stamina without waiting for backoffs?

The README says that tests can deactivate retries globally, limit the number of attempts, or remove backoffs. It does not document the exact API calls, so the project documentation is the source for the concrete syntax.

## Sources

- [hynek/stamina on GitHub](https://github.com/hynek/stamina)
- [License: MIT](https://github.com/hynek/stamina/blob/main/LICENSE)
- [Project website](https://stamina.hynek.me/)
- [README](https://github.com/hynek/stamina/blob/main/README.md)
- [Releases](https://github.com/hynek/stamina/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/hynek-stamina
