Skip to content

Python requests retry: retries, backoff and when to switch proxy

We timed every retry at our own server. requests retries nothing by default, the first retry comes at once, and POST is not retried unless you allow it.

HProxy TeamOctober 12, 2026Updated October 12, 20265 min read

Python requests retry: retries, backoff and when to switch proxy

The requests library does not retry anything on its own. To retry, you mount urllib3's Retry on a Session, and its settings decide how long each wait is.

We built a small server that fails on purpose and logs the time of every attempt it sees. Then we timed requests 2.34.2 against it on 12 October 2026, so the waits below are measured, not worked out from a formula.

By default, nothing is retried

Our server answered 503 twice before it would answer 200. Plain requests.get() made one attempt and returned the 503. Its default adapter shows why:

Retry(total=0, connect=None, read=False, redirect=None, status=None)

The requests documentation says the same: "By default, Requests does not retry failed connections".

A Session that retries

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

retry = Retry(
    total=5,
    backoff_factor=0.5,
    status_forcelist=[429, 500, 502, 503, 504],
)
session = requests.Session()
session.mount("http://", HTTPAdapter(max_retries=retry))
session.mount("https://", HTTPAdapter(max_retries=retry))

r = session.get("http://127.0.0.1:31293/fail/3/503?k=page1", timeout=10)
print(r.status_code)
200

The address is our test server, which failed three times here. total caps the retries, status_forcelist lists the answers worth retrying, and backoff_factor sets the waits. The server saw four attempts, and the fourth got its 200.

What backoff_factor really waits

Seconds after the first attempt, three 503s in a row
  • backoff_factor=0.5
  • backoff_factor=1
Source: HProxy test on our server, 12 October 2026: requests 2.34.2, urllib3 2.8.0, our own fault server · hproxy.comProxy.

The first retry came at once. After that, each wait doubled: 1 and 2 seconds with a factor of 0.5, and 2 and 4 seconds with a factor of 1. The urllib3 documentation calls it "A backoff factor to apply between attempts after the second try". Most errors, it says, clear up on a second try.

Some guides get this wrong. One puts "1, 2, 4 seconds" in a code comment for a factor of 1, where we measured 0, 2 and 4.

Retry-After on a 429

In the words of its standard, a 429 means "the user has sent too many requests in a given amount of time". The answer may carry a Retry-After header. We answered the first attempt with 429 and Retry-After: 3. With 429 in status_forcelist, the second attempt came 3.06 seconds later. urllib3 obeys the header by default. With respect_retry_after_header=False, it came after 0.05 seconds.

POST is not retried unless you allow it

With the Retry above, a POST that met a 503 was not retried at all. POST is missing from urllib3's default list, which holds DELETE, GET, HEAD, OPTIONS, PUT and TRACE. Those are the methods that are safe to repeat.

post_retry = Retry(total=5, backoff_factor=0.5, status_forcelist=[503], allowed_methods=None)
post_session = requests.Session()
post_session.mount("http://", HTTPAdapter(max_retries=post_retry))

r = post_session.post("http://127.0.0.1:31293/fail/2/503?k=page2", data={"x": "1"}, timeout=10)
print(r.status_code)
200

allowed_methods=None retries every method, and the POST went through on its third attempt. Use it only when sending the same POST twice is harmless, such as a search, and not for an order or a payment.

When the retries run out

When the server kept failing, requests raised RetryError by default. To get the last answer instead, set raise_on_status=False:

last_retry = Retry(total=2, status_forcelist=[503], raise_on_status=False)
last_session = requests.Session()
last_session.mount("http://", HTTPAdapter(max_retries=last_retry))

r = last_session.get("http://127.0.0.1:31293/fail/5/503?k=page3", timeout=10)
print(r.status_code)
503

Timeouts and dead servers

Retry also covers read timeouts on safe methods. Our server took 3 seconds to answer the first two attempts, and the client gave up after 1 second each time. The third attempt got a 200, 2.0 seconds after the first. A port where nothing listens failed for good after 3.0 seconds, the sum of its waits.

The error you get then says "Max retries exceeded", and so does a plain request with no retries set:

try:
    requests.get("http://127.0.0.1:31299/", timeout=2)
except requests.exceptions.ConnectionError as e:
    print(str(e)[:80])
HTTPConnectionPool(host='127.0.0.1', port=31299): Max retries exceeded with url:

The words come from urllib3, which wraps every final failure that way. The "Caused by" part that follows names the real cause.

When to switch proxy

Retrying the same proxy helps when the fault is passing. When a proxy keeps failing after its retries, try the next one:

def get_with_proxies(session, url, proxy_list, **kwargs):
    for proxy in proxy_list:
        try:
            return session.get(url, proxies={"http": proxy, "https": proxy}, **kwargs)
        except (requests.exceptions.ProxyError, requests.exceptions.ConnectionError):
            continue  # this proxy failed even after its retries: try the next one
    raise RuntimeError("every proxy failed")

PROXIES = ["http://127.0.0.1:31299", "http://127.0.0.1:31281"]  # the first one is dead
r = get_with_proxies(session, "http://127.0.0.1:31293/fail/0/200?k=page5", PROXIES, timeout=5)
print(r.status_code)
200

The first proxy was dead and raised ProxyError; the second, 3proxy on our own server, delivered the 200. A 429 is a different case: the site limits your address, so a new address helps where waiting would not. Our guide to fixing 429 errors covers when to wait and when to rotate.

Here is every case from our test, with each attempt as the server saw it:

Every retry case, with the time of each attempt at our fault server
Our own capture, 12 October 2026: retry_tests.py on our server (long lines cut)

What this page could not check

  • One version of requests and urllib3, against our own server on the same machine. Real networks add their own delays.
  • The proxy switch used a closed port and a local 3proxy, not a proxy network.
  • We did not test jitter, the tenacity library or async clients.
  • We will rerun these tests by 12 January 2027.

Sources

  • urllib3 documentation, urllib3.util.Retry: urllib3.readthedocs.io.
  • urllib3 2.8.0 source, Retry.get_backoff_time: github.com.
  • Requests documentation, Developer Interface (HTTPAdapter): requests.readthedocs.io.
  • RFC 6585, 429 Too Many Requests (April 2012): rfc-editor.org, and RFC 9110, Retry-After (June 2022): rfc-editor.org.
  • Our own tests on our server, 12 October 2026: retry_tests.py at 01:14 UTC and the code on this page at 01:15 UTC, against our own fault server.

Frequently asked questions

How many times does Python requests retry by default?

None. Against a server that failed twice, plain requests made one attempt and returned the 503. Its default adapter holds Retry(total=0, read=False). Mount an HTTPAdapter with your own Retry on a Session to change that.

How do I retry a 429 with its Retry-After header?

Put 429 in status_forcelist. urllib3 obeys the Retry-After header by default: with Retry-After: 3, our second attempt came 3.06 seconds after the first.

How does backoff_factor work in requests?

urllib3 waits nothing before the first retry, then backoff_factor times 2, then times 4, and so on. With 0.5 our retries came after 0, 1 and 2 seconds; with 1, after 0, 2 and 4 seconds.

How do I retry a POST request?

Retry skips POST by default, because repeating a POST can do the same thing twice. To retry it anyway, pass allowed_methods=None, and only for requests that are safe to repeat. Ours then went through on the third attempt.

What does 'Max retries exceeded with url' mean?

It is how urllib3 reports a request that failed for good, and it appears even when no retries were set: a plain requests.get to a closed port said it too. Read the 'Caused by' part for the real reason, such as a refused connection.

Get proxies that are alive right now

That list re-checks every exit every few minutes across 100+ countries, with a live last-checked time, so you copy IPs that worked moments ago, not a stale text dump. When the location has to survive a real check, the paid network holds up.

129M+ proxy checks run · 100+ countries · HTTP / HTTPS / SOCKS · re-checked every few minutes · no signup

HProxy.

Honest guides and comparisons on proxies, scraping and staying unblocked, from the team that runs the network.

RSS feed