Python requests: Max retries exceeded (tested)

ConnectionError: HTTPSConnectionPool(...): Max retries exceeded with url is a wrapper. The real cause is in the Caused by part at the end. We reproduced six causes with requests 2.34.2 and show the exact message for each, plus timeouts and retries that we measured.

1. Read the "Caused by" part, not "Max retries"

requests prints the same prefix for many different failures. The useful part is the class inside Caused by (...). "Max retries exceeded" shows up even with no retries at all. By default requests uses 0 retries, and every message in the table below came from a plain requests.get() with the default setting.

2. The exact errors we got

SituationException classCaused by (exact text)Time to fail
Nothing listening on the portConnectionErrorNewConnectionError(... Failed to establish a new connection: [Errno 111] Connection refused)0.00 s
Host name does not existConnectionErrorNameResolutionError(... Failed to resolve 'no-such-host.invalid' ([Errno -2] Name or service not known))0.01 s
Self-signed HTTPS certificateSSLErrorSSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate (_ssl.c:1029)')0.03 s
Cert trusted, but wrong host nameSSLError... Hostname mismatch, certificate is not valid for 'localhost'.0.00 s
Server accepts but never answers, timeout=(3, 1)ReadTimeoutNo "Max retries" text: HTTPConnectionPool(host='127.0.0.1', port=8983): Read timed out. (read timeout=1)1.00 s
Server closes the socket without a replyConnectionErrorNo "Max retries" text: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))0.00 s
Server never answers, no timeoutnoneStill waiting when we killed it at 3 sforever

Full message for the refused case:

requests.exceptions.ConnectionError: HTTPConnectionPool(host='127.0.0.1', port=8985):
Max retries exceeded with url: / (Caused by NewConnectionError("HTTPConnection(host='127.0.0.1',
port=8985): Failed to establish a new connection: [Errno 111] Connection refused"))

SSLError is a subclass of ConnectionError, so except ConnectionError also catches it. ReadTimeout is not. It inherits from Timeout and RequestException. Catch requests.RequestException if you want all of them.

3. Fix by cause

4. Always set a timeout

requests has no default timeout. Our test call to a server that accepted the connection but never replied was still hanging when timeout 3 killed it. Use a tuple: the first number is the connect timeout, the second is the read timeout.

r = requests.get(url, timeout=(3, 10))  # 3 s to connect, 10 s between bytes

With timeout=(3, 1) the slow server raised ReadTimeout after exactly 1.00 s.

5. Add real retries with HTTPAdapter and Retry

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

s = requests.Session()
retry = Retry(total=3, backoff_factor=0.5,
              status_forcelist=[502, 503, 504],
              allowed_methods=None)  # None = retry all methods, incl. POST
s.mount("http://", HTTPAdapter(max_retries=retry))
s.mount("https://", HTTPAdapter(max_retries=retry))
r = s.get(url, timeout=(3, 10))

What we measured with total=3, backoff_factor=0.5 (connect timeout 3 s, read timeout 1 s where noted):

Server behaviourServer hitsGaps between hitsTotal timeFinal exception
Closes without reply40.0, 1.0, 2.0 s3.01 sConnectionError ... Caused by ProtocolError('Connection aborted.', RemoteDisconnected(...))
Always 50340.0, 1.0, 2.0 s3.01 sRetryError ... Caused by ResponseError('too many 503 error responses')
Never answers, read timeout 1 s41.0, 2.0, 3.0 s (1 s timeout + backoff)7.01 sConnectionError ... Caused by ReadTimeoutError(... Read timed out. (read timeout=1))
Port closed (refused)0-3.00 sConnectionError ... Caused by NewConnectionError(... Connection refused)

Things to notice:

Only retry requests that are safe to repeat. Per the urllib3 docs (not measured), the default allowed_methods excludes POST and PATCH. We set it to None only for the test.

Frequently asked questions

Why does requests say Max retries exceeded when I never set retries?

Because the message comes from urllib3, which wraps every connect error in MaxRetryError. With the default 0 retries we still got "Max retries exceeded with url" for refused, DNS and SSL errors after one attempt.

How do I see the real cause?

Read the text after "Caused by". NewConnectionError with Connection refused means nothing listens on the port. NameResolutionError means DNS. SSLError means the certificate failed. ReadTimeoutError means the server was too slow.

What timeout should I use with requests?

Always pass one, because there is no default. Our call without a timeout was still waiting at 3 s and would wait forever. A tuple like timeout=(3, 10) sets 3 s to connect and 10 s to read.

Does except requests.ConnectionError catch timeouts?

Not a plain ReadTimeout: it inherits from Timeout. But SSLError is a ConnectionError, and after Retry runs out a read timeout comes back as ConnectionError. Catch requests.RequestException to cover all of them.

How many times does Retry(total=3) hit the server?

4 times: the first try plus 3 retries. With backoff_factor=0.5 we measured gaps of 0, 1 and 2 s, so about 3 s in total before the final error.

Written with AI assistance. Measured on 2026-10-02 with requests 2.34.2, urllib3 2.8.0 and Python 3.13.5 against local Python socket servers (plain HTTP and a self-signed TLS server made with openssl). Error strings below are copied from the output. PlainHeaders is our extension; requests, urllib3 and Python are free.