2 September 2026

Retrying an HTTP request in Python, properly

Retrying an HTTP request in Python, properly

Most hand-written retry loops get three things wrong: they retry errors that will never succeed, they retry on a fixed interval, and they have no ceiling. The first wastes time, the second turns a struggling service into a failing one, and the third is how a script runs all night.

import random
import time
import requests

RETRY_ON = {408, 429, 500, 502, 503, 504}

def get(url, *, attempts=5, timeout=10):
    """GET with exponential backoff and jitter.

    Retries only what a retry can plausibly fix. A 404 or a 401 will be a
    404 or a 401 next time too, so those raise immediately.
    """
    last = None
    for attempt in range(attempts):
        try:
            response = requests.get(url, timeout=timeout)
            if response.status_code not in RETRY_ON:
                response.raise_for_status()
                return response
            last = requests.HTTPError(f"{response.status_code} for {url}")

            # Honour Retry-After when the server sends one - it knows
            # better than any backoff formula does.
            retry_after = response.headers.get("Retry-After")
            if retry_after and retry_after.isdigit():
                time.sleep(min(int(retry_after), 60))
                continue
        except (requests.Timeout, requests.ConnectionError) as exc:
            last = exc

        if attempt == attempts - 1:
            break

        # Exponential, capped, with jitter. Without the jitter every
        # client that failed together retries together, and the thundering
        # herd is what keeps the service down.
        delay = min(2 ** attempt, 30) * (0.5 + random.random())
        time.sleep(delay)

    raise last

The three decisions worth understanding

Retry only what a retry can fix. 408, 429 and the 5xx family are transient. A 401 means your credentials are wrong and a 404 means the thing is not there - retrying either just delays the error you were always going to get.

Jitter is not optional. If a hundred clients fail at the same instant and all back off by exactly two seconds, they all return at the same instant. The random multiplier spreads them out, and it is the single line that separates a polite client from a denial-of-service.

Respect Retry-After. When a server sends it, it is telling you exactly when to come back. Any formula you use instead is a guess against information you already have.

If you are using requests anyway, urllib3's built-in Retry adapter does most of this - the value in writing it out is knowing what it is doing on your behalf.