Skip to main content

http-client-retries

PyPI Python License

English | Русский

HTTP request retries for cloudru-qa-http-client.

Network failures, timeouts, and temporary server errors are common when calling external services. This library handles retries: add one option to the client and it resends requests according to the configured policy. There is no need to write loops and delays manually.

Supports synchronous and asynchronous clients, with both regular and streaming requests. Built on tenacity, which provides the stop and wait strategies.

Installation

Requires Python 3.13 or later.

pip install cloudru-qa-http-client-retries

Import the library as the module http_client_retries.

Skills for AI agents

The package includes the http-client-retries-usage skill with examples and usage guidance for AI agents. Install the library in your project environment, then run from the project root (requires uv):

uvx library-skills install --skill http-client-retries-usage --yes

For Claude Code, add --claude. Instructions are bundled with the library version.

Quick start

Add the option to the client constructor to retry this client's requests on failure:

from http_client import HttpClient
from http_client_retries import options

with HttpClient("https://api.example.com", options.with_retries_failed_requests()) as client:
    response = client.get("/orders/42")

The asynchronous client works the same way:

from http_client import AsyncHttpClient

async with AsyncHttpClient("https://api.example.com", options.with_retries_failed_requests()) as client:
    response = await client.get("/orders/42")

When to retry

Connection errors

with_retries_failed_requests retries requests when the server cannot be reached:

from http_client import HttpClient
from http_client_retries import options

client = HttpClient("https://api.example.com", options.with_retries_failed_requests())

Retries httpx.RequestError (including timeouts and dropped connections), httpcore.NetworkError, and httpcore.PoolTimeout. Other errors propagate immediately without retries.

Errors that retries cannot resolve are excluded, so they do not waste the retry budget and waiting time:

  • httpx.TooManyRedirects: a retry follows the same redirect chain;
  • httpx.UnsupportedProtocol: the URL scheme does not change;
  • httpx.DecodingError: the server returns the same unreadable body.

Retries are not limited to idempotent methods. POST and PATCH are retried just like GET. A network error does not prove that the server did not perform the operation: the request may have arrived, and the response connection may have failed. Keeping the same X-Request-ID across attempts prevents duplicates only if the server implements this behavior.

Limitation: request bodies must be replayable

A retry resends the same request, so its body must be readable again.

Request body Retried
bytes, str, json=, data= Yes
List or tuple of chunks Yes
Open file Yes; its position is restored before each attempt
files= with bytes or seekable files Yes
files= with a non-seekable file No
Generator or custom one-shot stream No

If the body cannot be replayed, the request runs exactly once and the original error propagates. The library returns an error rather than sending a truncated body to the server.

def stream_document():
    yield from produce_chunks()


client.send(client.client.build_request("POST", "/documents", content=stream_document()))

To retry such an upload, buffer the body in memory (content=b"".join(...)) or pass a file object.

The with_retries_if condition still runs once for these requests. Since the request cannot be replayed, its response is returned as is, even if the condition requested a retry.

Response conditions

with_retries_if retries while your condition returns True:

from httpx import Response


def is_throttled(response: Response) -> bool:
    return response.status_code == 429


client = HttpClient("https://api.example.com", options.with_retries_if(is_throttled))

The condition can be any function that takes an httpx.Response and returns a bool.

Combine these options with each other and with http_client options:

from http_client import options as http_options

client = HttpClient(
    "https://api.example.com",
    options.with_retries_failed_requests(),
    options.with_retries_if(lambda response: response.status_code >= 500),
    http_options.with_timeout(5.0),
)

Both options share a retry loop and attempt budget: with stop_after_attempt(5), at most five requests reach the server, even when network failures alternate with 5xx responses. Option order does not affect the result.

Set policy parameters once per client, with each parameter specified in exactly one option. Specifying the same parameter in both options makes the constructor raise ValueError. The library deliberately avoids comparing values semantically: tenacity strategies have no equality comparison, and treating hook functions as identical could silently discard one of them.

Streaming requests

No extra setup is needed: both options cover client.stream(...) and client.stream_request(...).

with HttpClient("https://api.example.com", options.with_retries_failed_requests()) as client:
    with client.stream("GET", "/large-file.zip") as response:
        for chunk in response.iter_bytes(chunk_size=8192):
            handle(chunk)

The with_retries_if condition runs on the open stream before reading the response body.

Only opening the stream is retried. If the connection fails while reading the body, inside iter_bytes() or aiter_bytes(), the error propagates immediately without retries. Some data has already been received, so restarting would be unsafe. Implement resuming downloads yourself using the Range header.

Retry policy configuration

Global settings

configure sets process-wide defaults:

from http_client_retries import Config

from http_client_retries import configure
from tenacity import stop_after_attempt, wait_exponential_jitter

configure(
    options=Config(
        stop=stop_after_attempt(5), wait=wait_exponential_jitter(initial=1, max=64, jitter=1), reraise=False
    ),
)

Without configure, the defaults in the parameter reference apply: up to five attempts with exponential backoff and random jitter.

Strategies come from tenacity; see stop and wait.

Per-client settings

To give one client a custom policy, pass it directly to the option:

from http_client_retries import RetryOverrides

from tenacity import stop_after_attempt

payments = HttpClient(
    "https://payments.internal",
    options.with_retries_failed_requests(
        overrides=RetryOverrides(stop=stop_after_attempt(2), reraise=True),
    ),
)

catalog = HttpClient(
    "https://catalog.internal",
    options.with_retries_failed_requests(),
)

Here, payments makes at most two attempts, while catalog uses the global configuration. Unspecified parameters also come from the global configuration, including changes made by configure after the client was created.

The Retry-After header

When the server sends Retry-After (usually with 429 or 503), the delay comes from that header. Both formats are supported: seconds and an HTTP date. The value is capped by max_retry_after. Invalid headers are ignored in favor of the regular wait strategy.

Since the header arrives in a response, only a policy that retries based on responses can use it. If a client retries only network errors, respect_retry_after and max_retry_after have no effect.

Disable it globally or for one client:

from http_client_retries import Config, RetryOverrides

configure(options=Config(respect_retry_after=False))

client = HttpClient(
    "https://api.example.com", options.with_retries_failed_requests(overrides=RetryOverrides(respect_retry_after=False))
)

After the last attempt

When attempts are exhausted, behavior depends on reraise:

reraise Exception raised
False (default) tenacity.RetryError; the original error is available through err.last_attempt.exception()
True The original error, such as httpx.ConnectError
from tenacity import RetryError

try:
    response = client.get("/orders/42")
except RetryError as err:
    original = err.last_attempt.exception()

reraise=True is useful when the caller already handles httpx exceptions:

from http_client_retries import RetryOverrides

import httpx

client = HttpClient(
    "https://api.example.com", options.with_retries_failed_requests(overrides=RetryOverrides(reraise=True))
)

try:
    response = client.get("/orders/42")
except httpx.ConnectError:
    ...

Retry logging

log_retry_error_hook logs the reason for each retry: method, URL, and error:

from http_client_retries import Config

import logging

from http_client_retries import configure, log_retry_error_hook

configure(options=Config(before_sleep_hook=log_retry_error_hook(logging.getLogger("http"))))

Replace it with a custom function that receives a RetryCallState:

from http_client_retries import Config


def report_attempt(retry_state):
    metrics.increment("http.retry", attempt=retry_state.attempt_number)


configure(options=Config(before_sleep_hook=report_attempt))

An asynchronous hook requires an asynchronous client: synchronous tenacity cannot await it, so this combination raises TypeError rather than silently skipping calls.

Disable the hook for one client by passing before_sleep_hook=None directly to the option; the global configuration stays unchanged.

Request identifiers

All attempts of one request share the same X-Request-ID and Traceparent. Find retries in logs by their shared identifier; the server can also use it to prevent duplicates. No configuration is needed:

client = HttpClient(
    "https://api.example.com",
    http_options.with_autogenerated_request_id(),
    options.with_retries_failed_requests(),
)

Identifiers set manually by the caller also stay the same across retries.

The rule is simple: the identifier belongs to the request object. Different requests get different values; all sends of one httpx.Request, including retries, share one identifier.

There is one limitation. Built-in httpx authentication schemes (BasicAuth, DigestAuth, or a function) reuse the request object, preserving the identifier. A custom httpx.Auth that creates a new Request instead of modifying the original gets a new identifier on each attempt: the library does not copy headers to the new request. If the server relies on X-Request-ID to prevent duplicates, set the header yourself in this case.

Retrying arbitrary functions

To retry calls outside http_client, use the decorators directly.

retry_if_failed retries connection errors:

import httpx

from http_client_retries import retry_if_failed


@retry_if_failed
def fetch_status():
    return httpx.get("https://api.example.com/health")

retry_if retries based on a result condition:

from http_client_retries import retry_if


@retry_if(lambda response: response.status_code == 503)
def fetch_report():
    return httpx.get("https://api.example.com/report")

Both also support asynchronous functions:

@retry_if_failed
async def fetch_status():
    async with httpx.AsyncClient() as client:
        return await client.get("https://api.example.com/health")

The decorators use the global configuration from configure.

Parameter reference

Accepted by both configure and the two options.

Parameter Default Purpose
stop stop_after_attempt(5) When to stop retrying
wait wait_exponential_jitter(initial=1, max=64, jitter=1) Delay between attempts
before_sleep_hook None Function called before each retry
reraise False Raise the original error instead of RetryError; does not affect response-condition retries
respect_retry_after True Use the Retry-After delay; requires a response-based retry policy
max_retry_after 60.0 Maximum Retry-After delay in seconds

get_config() returns an immutable snapshot: its fields cannot be reassigned. configure() replaces the entire configuration in one assignment. Immutability is shallow: tenacity strategies themselves remain mutable objects, so do not change their fields after passing them to configure(). Create a new strategy instead.

reraise applies only to network-error retries, where an original exception exists to raise again. When a response condition exhausts the retry budget, there is no original exception: RetryError is always raised, and the last response is available through err.last_attempt.result().

Development

This library is developed in the qa-libraries monorepo alongside the other cloudru-qa-* libraries. You need uv, GNU Make and checkmake; run the commands from the repository root:

make sync                            # dependencies for all libraries from uv.lock
make test-http-client-retries        # tests for this library
make typecheck-http-client-retries   # mypy for this library
make check                           # formatting, Ruff, mypy, and tests for the whole suite

Tests run on Python 3.13 and 3.14. Each version uses a separate environment: make sync test-http-client-retries PYTHON_VERSION=3.14.

Create a release with the Git tag http-client-retries/v<version>. The tag version must match the version in packages/http-client-retries/pyproject.toml.

Metadata

Release files for cloudru-qa-http-client-retries 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for cloudru-qa-http-client-retries 1.0.0
File Size Uploaded Provenance Actions
cloudru_qa_http_client_retries-1.0.0.tar.gz 41.5 kB With provenance Details for cloudru_qa_http_client_retries-1.0.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for cloudru-qa-http-client-retries 1.0.0
File Interpreter ABI Platform Actions
cloudru_qa_http_client_retries-1.0.0-py3-none-any.whl Python 3 none any Details for cloudru_qa_http_client_retries-1.0.0-py3-none-any.whl

Total release size: 76.9 kB

Release files / cloudru_qa_http_client_retries-1.0.0.tar.gz

Download URL cloudru_qa_http_client_retries-1.0.0.tar.gz
Size 41.5 kB
Tags Source
SHA-256 checksum
How to use checksums
5e59c55c75d2fc7e6f36d10d4c52b35a9aa55387d8bcf9b2b84dd7a7407916d0
BLAKE2b-256 checksum
How to use checksums
fd23b464f64f4f20dca38b3edc69a839803e5fdc0400190b3c72047bf3ff6bf9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 14, 2026.

Transparency log

Release files / cloudru_qa_http_client_retries-1.0.0-py3-none-any.whl

Download URL cloudru_qa_http_client_retries-1.0.0-py3-none-any.whl
Size 35.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
837a9e4555b13a6e13fc6b73656fd70bec13960c65b0c82a6dee574cd715e1c8
BLAKE2b-256 checksum
How to use checksums
19cd13dd6af2afade918a3116063b27668a2d6e400258a2ca2bda8aaef62d941
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 14, 2026.

Transparency log

Security for cloudru-qa-http-client-retries 1.0.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for cloudru_qa_http_client_retries-1.0.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 14, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page