Skip to content

Retries

async with HTTPClient(base_url="https://api.example.com/", max_retries=3) as client:
    await client.get("items/7")

Implemented as a real pyreqwest with_middleware hook — retries on a transport failure (including a body cut off partway) or a 429/500/502/503/504 response, with exponential backoff, honoring a Retry-After header when the server sends one.

By default, only the idempotent verbs retry: get, put, delete, head. post/patch need an explicit opt-in, since retrying a non-idempotent request can duplicate side effects:

async with HTTPClient(
    base_url="https://api.example.com/",
    max_retries=3,
    retry_methods={"GET", "POST"},
) as client:
    ...

Backoff

The wait between attempts is backoff_base * 2 ** attempt, so the default backoff_base=0.1 waits 0.1s, then 0.2s, then 0.4s. A Retry-After header on the response wins over the computed delay, up to max_retry_after (default 60s). A server asking for a longer wait ends retrying: its 429/503 is raised as HTTPResponseError, so you can schedule a retry from e.headers["Retry-After"] rather than a call silently blocking for as long as the server likes. Pass max_retry_after=None to honour any wait:

async with HTTPClient(
    base_url="https://api.example.com/",
    max_retries=3,
    backoff_base=0.5,  # 0.5s, then 1.0s, then 2.0s
) as client:
    ...

What is never retried

A request that could never be built — a non-https URL under https_only=True, a malformed URL — fails immediately rather than being retried, since no amount of waiting changes the outcome. It still raises the same HTTPTransportError as any other transport failure.

This applies to sse()'s reconnect loop too: a permanently unbuildable request raises on the first attempt instead of working through max_reconnects × reconnect_delay first.

A redirect loop (past max_redirects) isn't retried either. sse()'s reconnect loop does reconnect on one, since a redirect loop can be transient, but a plain verb raises it straight away.

Nor is a body that can never be decoded, such as a corrupt gzip body: it comes back the same on every attempt, so it raises HTTPConnectionError straight away.

A body cut off partway

A connection that dies partway through the body is retried like any other transport failure, including when the body is shorter than its Content-Length promised (what a server or pod dying mid-response usually looks like). pyreqwest reports that case as a decode error, the same type as a corrupt gzip body, so lothc tells the two apart by its cause: only a failed read from the connection is retried.

retry_methods is case-insensitive ({"get"} works), and an empty collection means "retry nothing": with max_retries set, pass retry_methods=set() to switch retries off per client.