FAQs
On this page are some examples of usage and commonly asked questions.
Chaining transports
HTTPX Retries is implemented as a custom Transport. It's common to want to add additional custom behaviour (eg. rate-limiting, proxies and more), and it's possible to chain transports to layer in behaviours; the first argument to the RetryTransport constructor is a transport to wrap.
with AsyncClient(
transport=RetryTransport(
RateLimitTransport(
AsyncHTTPTransport(),
interval=timedelta(seconds=1),
count=3,
),
retry=Retry(total=5, backoff_factor=0.5),
),
timeout=90.0,
) as client:
...
Client timeout or Retry timeout?
Does the httpx.Client timeout apply to each request made in a series of retries, or to the "user request", who wants
the result in the next 10s, regardless of how many times the underlying HTTP client needs to try to get it?
The timeout passed to httpx.Client applies to each request in a series of retries. If a httpx.TimeoutException is
raised and a retry is applicable, the same request, with the same timeout values, will be retried.
In the case where retries are bounded by individual (client) timeouts, it is always possible to work out the maximum possible time taken to execute all retries.
So if you wanted to add a lower overall retry value than the number of retries multiplied by the client timeout, you could tweak the max_backoff_wait and backoff_factor. Eg.
Retry(total=3, max_backoff_wait=5.0, backoff_factor=1, jitter=0)
would result in a retry-inclusive timeout of 15s + 3 * (client_timeout).
Why wasn't my ReadTimeout retried?
If an error like httpx.ReadTimeout or httpx.RemoteProtocolError("peer closed connection without sending complete message body") escapes your client with no retry attempts logged, this is expected given HTTPX's transport architecture, not a bug in RetryTransport.
HTTPX transports return as soon as response headers are received. The response body is read lazily inside the client — by response.read(), response.aread(), or iteration over a streaming response. Errors that occur while reading the body are raised directly to the caller, bypassing the transport. Since RetryTransport only observes what flows through its handle_request / handle_async_request methods, it cannot retry these body-phase errors.
Retried by RetryTransport:
httpx.TimeoutException,httpx.NetworkError, andhttpx.RemoteProtocolErrorraised before response headers arrive (for example, a connect timeout, or a slow server that doesn't send headers in time).- Responses with a retryable status code (default:
429,502,503,504).
Not retried by RetryTransport:
- Any exception raised during
response.read(),response.aread(), or iteration of a streaming response — includingReadTimeoutmid-body andRemoteProtocolError("peer closed connection...").
If you need to retry body-phase errors today, do it at the call site:
import httpx
retryable = (httpx.ReadTimeout, httpx.RemoteProtocolError)
for attempt in range(5):
try:
response = client.get("https://example.com")
break
except retryable:
if attempt == 4:
raise
Limits / Cert / SSL / http2 parameters passed to the client are not being applied
This is a limitation of the way transports are applied to clients in HTTPX. If you provide a custom transport, several parameters
that can be passed to httpx.Client are ignored. The workaround is to directly provide an instance of
httpx.HTTPTransport (or the async variant) which
will accept these parameters.
with Client(
transport=RetryTransport(
HTTPTransport(
# Pass a transport with these parameters to wrap.
http2=True,
limits=httpx.Limits(max_connections=100, max_keepalive_connections=100, keepalive_expiry=60),
verify=False
),
retry=Retry(total=5, backoff_factor=0.5),
)
# These will do nothing!
http2=True,
limits=httpx.Limits(max_connections=100, max_keepalive_connections=100, keepalive_expiry=60),
verify=False
) as client:
...