Retry

The @retry decorator repeats a request automatically when it raises a selected exception or returns a selected HTTP status, waiting longer between each attempt. Stack it on a request method the same way as @AsyncSession.single_session or a hook decorator.

Retry Example
 1from typing import Annotated, Any
 2
 3from ahttp_client import (
 4    AsyncSession,
 5    HTTPServerError,
 6    Path,
 7    Response,
 8    get,
 9    retry,
10)
11
12
13class GitHubService(AsyncSession):
14    async def after_request(self, response: Response) -> Response:
15        response.raise_for_status()
16        return response
17
18    @retry(
19        max_retries=3,
20        backoff_factor=0.5,
21        retry_on=(HTTPServerError, TimeoutError),
22        retry_on_status=(502, 503, 504),
23        max_delay=4.0,
24    )
25    @get("/users/{user}/repos")
26    async def list_repositories(
27        self, response: Response, user: Annotated[str, Path]
28    ) -> list[dict[str, Any]]:
29        return response.json()

Exception-based retries use retry_on. Calling Response.raise_for_status() in a session-level after_request() hook turns a 4xx or 5xx response into the matching HTTPException subclass (see Exception), which retry_on can catch. Alternatively, raise_on=True on a request treats every status other than 200 as a failure before the request result is returned.

Status-based retries use retry_on_status and do not require an exception or an after_request() hook. Use it when a response status itself should be retried while still allowing the final response to be returned normally:

@retry(max_retries=3, retry_on_status=(429, 502, 503, 504))
@get("/users/{user}/repos")
async def list_repositories(self, response: Response, user: Annotated[str, Path]):
    return response.json()

Backoff and Retry Scope

The defaults retry up to three times after the initial request, triggered by HTTPServerError. To make a returned 5xx response raise that exception, use Response.raise_for_status() or raise_on=True. retry_on_status defaults to an empty tuple.

The wait before retry attempt n is backoff_factor * 2 ** (n - 1) seconds, capped by max_delay when it is set. With the defaults above (backoff_factor=0.5, max_delay=4.0), the delays are 0.5, 1.0, 2.0, and 4.0 (capped) seconds.

retry_on accepts one exception class or a tuple, so transport-level failures such as TimeoutError can be added alongside HTTPServerError, or used in its place. retry_on_status likewise accepts one HTTP status code or a tuple of status codes from 100 through 599. Responses from attempts that will be retried are closed automatically. When the retry budget is exhausted, the final matching-status response is not pre-closed and proceeds through normal response handling.

Warning

Exception retries cover failures from request transport, a session-level after_request() hook, and raise_on=True validation. Status retries inspect the response before request-level after_hook callbacks run. A request-level after_hook (see Pre Hooking / Post Hooking) runs after the retry operation is already done, so anything it raises never gets retried.

Note

max_retries, backoff_factor, and max_delay all need to be finite, non-negative values. Bad values raise TypeError or ValueError as soon as @retry is applied.

Warning

Automatic retries are limited to idempotent HTTP methods. Retrying a POST, PATCH, or another non-idempotent request can duplicate a server-side operation, so it must be explicitly enabled with retry_unsafe=True.

Decoration Order

@retry works whether it sits above or below @request (or @get, @post, etc.) on the same method — both orders end up with the same retry configuration:

# @retry above @request: applied directly to the built RequestCore.
@retry(max_retries=5, backoff_factor=0.2)
@get("/users/{user}/repos")
async def list_repositories(self, response: Response, user: Annotated[str, Path]):
    return response.json()

# @retry below @request: stashed until @request builds the RequestCore.
@get("/users/{user}/repos")
@retry(max_retries=5, backoff_factor=0.2)
async def list_repositories(self, response: Response, user: Annotated[str, Path]):
    return response.json()

Reference

class RetryConfig

Configure retry attempts, exception/status matching, and exponential backoff.

Parameters:
  • max_retries (int) – Maximum number of attempts after the initial request.

  • backoff_factor (float) – Multiplier used to calculate the exponential delay before each retry.

  • retry_on (tuple[type[Exception], ...]) – Exception classes that trigger another attempt.

  • max_delay (Optional[float]) – Maximum delay in seconds, or None for no upper bound.

  • retry_unsafe (bool) – Whether retries are explicitly allowed for non-idempotent HTTP methods such as POST and PATCH.

  • retry_on_status (tuple[int, ...]) – HTTP response statuses from 100 through 599 that trigger another attempt without requiring an exception to be raised.

Raises:
  • TypeError – If a retry count, delay, or retry filter has an invalid type.

  • ValueError – If a retry count or delay is negative or non-finite.

Notes

max_retries excludes the initial request. The delay before retry attempt n is backoff_factor * 2 ** (n - 1) and is capped by max_delay when an upper bound is configured.

async execute_async(make_request_func: Callable[[...], Awaitable[tuple[Response, Any]]]) → tuple[Response, Any]

Execute an asynchronous request and retry matching failures.

Parameters:

make_request_func (Callable[..., Awaitable[tuple[Response, Any]]]) – Zero-argument callable that returns the raw response and the response-pipeline result.

Returns:

Raw response and processed result from the final attempt. A matching status response is returned when its retry budget is exhausted.

Return type:

tuple[Response, Any]

Raises:

Exception – Re-raises a non-matching exception immediately or the last matching exception after the retry budget is exhausted.

execute_sync(make_request_func: Callable[[...], tuple[Response, Any]]) → tuple[Response, Any]

Execute a synchronous request and retry matching failures.

Parameters:

make_request_func (Callable[..., tuple[Response, Any]]) – Zero-argument callable that returns the raw response and the response-pipeline result.

Returns:

Raw response and processed result from the final attempt. A matching status response is returned when its retry budget is exhausted.

Return type:

tuple[Response, Any]

Raises:

Exception – Re-raises a non-matching exception immediately or the last matching exception after the retry budget is exhausted.

@retry(max_retries: int = 3, *, backoff_factor: float = 1.0, retry_on: tuple[type[Exception], ...] | type[Exception] = (<class 'ahttp_client.exception.HTTPServerError'>,), retry_on_status: tuple[int, ...] | int = (), max_delay: float | None = None, retry_unsafe: bool = False) → RequestDecorator[Any, Any]

Decorate a request to retry on failure with exponential backoff.

Parameters:
  • max_retries (int) – Maximum number of retry attempts after the initial request.

  • backoff_factor (float) – Multiplier applied to the exponential backoff delay. delay = backoff_factor * 2 ** (attempt - 1)

  • retry_on (tuple[type[Exception], ...] | type[Exception]) – Exception types that trigger a retry attempt. Defaults to HTTPServerError (5xx responses).

  • retry_on_status (tuple[int, ...] | int) – HTTP response statuses that trigger a retry attempt without an exception. Defaults to an empty tuple.

  • max_delay (Optional[float]) – Upper bound on the sleep delay in seconds. None means no cap.

  • retry_unsafe (bool) – Explicitly allow retries for non-idempotent methods. Defaults to False so POST, PATCH, and other unsafe requests are not repeated accidentally.

Returns:

Decorator that attaches the retry configuration to a request.

Return type:

RequestDecorator[Any, Any]

Raises:
  • TypeError – If a retry count, delay, or retry filter has an invalid type.

  • ValueError – If a retry count or delay is negative or non-finite.

Notes

Exceptions raised by request transport or a session-level after_request hook are eligible for retry. retry_on_status checks returned raw response statuses without requiring an exception. Request-level after_hook callbacks run after the retry operation and are outside its scope.