Geonode logo
Library and script errors

How to Fix Max Retries Exceeded With URL in Python

Name the failure hidden behind the wrapper, then fix it or let requests retry it with sensible waits.

Updated

TL;DR

Max retries exceeded with url means urllib3, the library under Python requests, stopped trying one request; with requests' defaults that happens after one failed attempt. Fix the error named after Caused by, often a host name that does not resolve or a port where nothing listens.

What Max retries exceeded looks like

Captured on macOS; Linux prints [Errno 111] for a refused port and [Errno -2] for an unknown name.

WhereWhat you see
requests 2.33 with urllib3 2.6, host name does not resolverequests.exceptions.ConnectionError: HTTPSConnectionPool(host='api.example.invalid', port=443): Max retries exceeded with url: /v1/items (Caused by NameResolutionError("HTTPSConnection(host='api.example.invalid', port=443): Failed to resolve 'api.example.invalid' ([Errno 8] nodename nor servname provided, or not known)"))
requests 2.31 with urllib3 1.26, nothing listens on the portrequests.exceptions.ConnectionError: HTTPConnectionPool(host='127.0.0.1', port=8000): Max retries exceeded with url: /health (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x1045e48c0>: Failed to establish a new connection: [Errno 61] Connection refused'))
requests with a Retry that allows read retries, server never answersrequests.exceptions.ConnectionError: HTTPConnectionPool(host='127.0.0.1', port=52812): Max retries exceeded with url: / (Caused by ReadTimeoutError("HTTPConnectionPool(host='127.0.0.1', port=52812): Read timed out. (read timeout=1)"))
requests with Retry(status_forcelist=[503]), every reply is 503requests.exceptions.RetryError: HTTPConnectionPool(host='127.0.0.1', port=41426): Max retries exceeded with url: /status (Caused by ResponseError('too many 503 error responses'))
urllib3 2.6 used directly, after its default 3 retriesurllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='127.0.0.1', port=8000): Max retries exceeded with url: / (Caused by NewConnectionError("HTTPConnection(host='127.0.0.1', port=8000): Failed to establish a new connection: [Errno 61] Connection refused"))
requests 2.33 with Retry(total=2, connect=2) and logging.basicConfig() onWARNING:urllib3.connectionpool:Retrying (Retry(total=1, connect=1, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError("HTTPConnection(host='127.0.0.1', port=62286): Failed to establish a new connection: [Errno 61] Connection refused")': /

Why this happens

urllib3 ran out of attempts, and the message wraps the error that stopped it.

Unless you mount your own Retry, requests allows urllib3 zero retries, so the first refused connection, failed DNS lookup or TLS error raises MaxRetryError. Its reason, printed after Caused by, is the error that ended the last attempt.

Diagnose your Max retries exceeded first

The exception names in the traceback tell you which step failed and whether retrying can help.

  • Open the error's host and path in a browser on this computer; if it loads, the server is up and your script's setup differs.

  • Read the class after Caused by; NameResolutionError means the host name did not resolve, and NewConnectionError with Connection refused means nothing listens on that port.

  • Read the class before HTTPSConnectionPool; ProxyError means the hop to your proxy failed, and SSLError means the TLS handshake failed.

  • Add logging.basicConfig() before the request; each Retrying line is one extra attempt, and no such line means requests tried only once.

  • If Caused by shows ResponseError('too many 503 error responses'), the server did answer, and your Retry's status_forcelist turned its replies into the error.

Solutions ranked by effectiveness

Correct the address first; retries help only when the same call works on a later attempt.

  1. Most common fix

    Correct the host, port or scheme

    Applies when Caused by names NameResolutionError, Connection refused or WRONG_VERSION_NUMBER. These fail the same way on every attempt, so fix the URL or the server before adding retries.

    1. Compare host and port in HTTPSConnectionPool(...) with the address the server really uses.

    2. NameResolutionError: correct the spelling; [Errno -3] instead means DNS fails where the script runs.

    3. Connection refused: start the server, or change the URL to the port it listens on.

    4. WRONG_VERSION_NUMBER: write http:// for a port that does not speak TLS.

  2. Check next

    Count connect, read and status retries apart

    Applies when the same call works on a later try, such as during a server restart. Give each failure type its own count, and catch RequestException: a RetryError is not a ConnectionError.

    # pip install requests
    from requests import RequestException, Session
    from requests.adapters import HTTPAdapter
    from urllib3.util import Retry
    retry = Retry(total=5, connect=3, read=1, status=2, backoff_factor=1,
                  status_forcelist=[502, 503, 504])
    session = Session()
    session.mount("https://", HTTPAdapter(max_retries=retry))
    try:
        print(session.get("https://example.com/", timeout=(5, 30)).status_code)
    except RequestException as err:
        print(type(err).__name__, "caused by", type(getattr(err.args[0], "reason", err)).__name__)
  3. For developers

    Keep the last reply when retries end

    Applies when status retries run out on 5xx replies. raise_on_status=False returns the final response, so your code can read its status and Retry-After header instead of catching RetryError.

    # pip install requests
    import requests
    from requests.adapters import HTTPAdapter
    from urllib3.util import Retry
    
    retry = Retry(total=3, status_forcelist=[502, 503, 504], backoff_factor=1,
                  raise_on_status=False)
    session = requests.Session()
    session.mount("https://", HTTPAdapter(max_retries=retry))
    r = session.get("https://example.com/", timeout=(5, 30))
    print(r.status_code, "Retry-After:", r.headers.get("Retry-After"))

Stop the Max retries exceeded from coming back

Habits that keep retries honest and the real cause visible in your logs.

  1. Reuse one Session per worker, so requests keeps connections open instead of connecting and resolving again for each call.

  2. Leave POST out of allowed_methods unless the API ignores duplicates; a retried read can run it twice.

  3. Pin urllib3 in requirements; release 2.6 changed how connections print in these errors, which can break log parsers.

  4. Track len(r.raw.retries.history) on successful responses; a rising count warns before calls start failing.

  5. Keep backoff_factor small enough that the waits fit your job; urllib3 caps a single wait at 120 seconds.

Proxy errors worth a retryGeonode's error reference says to retry its 5xx codes with exponential backoff.
Try residential proxies

Related errors

Learn more

FAQ

It means urllib3 gave up on one request after using its allowed attempts, and requests passed that on. The exception named after Caused by, such as NewConnectionError or SSLError, is the actual problem to fix.

No. The limit applies to the attempts behind one call, not to your request rate. Heavy traffic can still cause it: on Linux, [Errno 99] Cannot assign requested address means every local ephemeral port is in use.

requests catches it and raises its own class instead, such as ConnectionError, ConnectTimeout, ProxyError, SSLError or RetryError. The original MaxRetryError is err.args[0], and its reason attribute holds the error printed after Caused by.

Mount HTTPAdapter(max_retries=3) on a Session for http:// and https://. The integer becomes Retry(3) with no backoff, and it also repeats GET read timeouts, although requests' docstring says data that reached the server is never retried.

Under requests' defaults a slow server raises ReadTimeout, and a slow connect raises ConnectTimeout. Once your Retry allows read retries, the last read timeout arrives as ConnectionError with Caused by ReadTimeoutError, so except ReadTimeout misses it.

pip retries a failed connection 5 times by default (--retries), printing one WARNING line each time. Then it reports No matching distribution found, which hides the cause, so read the NewConnectionError text inside those lines.

Keep your Retry, add a proxy

Set session.proxies once; the Retry mounted on that session also covers proxied calls.