httpx retry: what HTTPTransport(retries=) does and doesn't retry (tested)

httpx.HTTPTransport(retries=3) retries only connection failures (ConnectError/ConnectTimeout). A ReadTimeout or a 429/503 response is not retried; handle those with a small loop that honours Retry-After.

httpx has one built-in retry setting, httpx.HTTPTransport(retries=N), and it covers less than people expect. Measured with httpx 0.28.1 on Python 3.13.5 against local http.server instances (srv.start(): the path is the seconds to wait before answering).

1. retries= retries failed connections

import httpx, socket, threading, time

# a port that refuses twice, then starts listening
port = 8966
def late_server():
    time.sleep(1.2)
    from srv import start
    start(port)
threading.Thread(target=late_server, daemon=True).start()

for retries in (0, 3):
    t = time.monotonic()
    transport = httpx.HTTPTransport(retries=retries)
    try:
        r = httpx.Client(transport=transport).get(f"http://127.0.0.1:{port}/0")
        print(f"retries={retries}: {r.status_code} after {time.monotonic() - t:.1f}s")
    except httpx.ConnectError as e:
        print(f"retries={retries}: ConnectError after {time.monotonic() - t:.1f}s")

Output:

retries=0: ConnectError after 0.0s
retries=3: 200 after 1.5s

The server starts listening 1.2 s in. Without retries the request fails at once with ConnectError; with retries=3 httpx reconnected with a short backoff and got the 200 after 1.5 s. Use httpx.AsyncHTTPTransport(retries=...) for AsyncClient.

2. It does not retry a read timeout

import httpx, time
from srv import start
base = start()   # /3 answers after 3 s

client = httpx.Client(transport=httpx.HTTPTransport(retries=3), timeout=1.0)
t = time.monotonic()
try:
    client.get(base + "/3")
except httpx.ReadTimeout:
    print(f"ReadTimeout after {time.monotonic() - t:.1f}s (not retried)")

Output:

ReadTimeout after 1.0s (not retried)

Once connected, a slow response raises ReadTimeout after the 1 s timeout with no retry, even with retries=3.

3. It does not retry 429/503 either: add a small loop

import httpx, threading, http.server, time

hits = 0
class H(http.server.BaseHTTPRequestHandler):
    def log_message(self, *a): pass
    def do_GET(self):
        global hits; hits += 1
        code = 503 if hits < 3 else 200
        self.send_response(code); self.send_header("Retry-After", "1"); self.send_header("Content-Length", "0"); self.end_headers()
s = http.server.ThreadingHTTPServer(("127.0.0.1", 8967), H)
threading.Thread(target=s.serve_forever, daemon=True).start()

client = httpx.Client(transport=httpx.HTTPTransport(retries=3))
print("transport retries only:", client.get("http://127.0.0.1:8967/").status_code)

def get_with_retry(client, url, attempts=4, statuses=(429, 502, 503, 504)):
    for attempt in range(attempts):
        r = client.get(url)
        if r.status_code not in statuses or attempt == attempts - 1:
            return r
        wait = float(r.headers.get("Retry-After", 2 ** attempt))
        print(f"  attempt {attempt + 1}: {r.status_code}, sleeping {wait}s")
        time.sleep(wait)

r = get_with_retry(client, "http://127.0.0.1:8967/")
print("with status retry:", r.status_code, "total hits:", hits)

Output:

transport retries only: 503
  attempt 1: 503, sleeping 1.0s
with status retry: 200 total hits: 3

A 503 comes back as a normal response, so the transport returns it. The loop retries listed status codes, honours a numeric Retry-After header (otherwise exponential backoff), and returns the last response when attempts run out. Only retry idempotent requests (GET, PUT, DELETE) this way, or a POST may run twice. To also retry timeouts, wrap the client.get in try/except httpx.TimeoutException inside the same loop; libraries such as tenacity do the same with decorators.

Related: httpx timeout default, httpx vs requests, requests ConnectionError max retries.

Frequently asked questions

Does httpx retry requests automatically?

No, not by default. HTTPTransport(retries=N) retries failed connections only.

How do I retry on 429 or 503 in httpx?

Check r.status_code in a loop, sleep for Retry-After (or exponential backoff), and retry; the transport's retries= option returns the 503 unchanged.

Does httpx retries= retry ReadTimeout?

No. In the test a ReadTimeout was raised after 1.0 s with retries=3 set.

Written with AI assistance. Measured on 2026-10-23 with httpx 0.28.1 on Python 3.13.5 against local http.server instances. Each block was run verbatim and the output pasted unedited. PlainHeaders is our extension; httpx is free.