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
| Situation | Exception class | Caused by (exact text) | Time to fail |
|---|---|---|---|
| Nothing listening on the port | ConnectionError | NewConnectionError(... Failed to establish a new connection: [Errno 111] Connection refused) | 0.00 s |
| Host name does not exist | ConnectionError | NameResolutionError(... Failed to resolve 'no-such-host.invalid' ([Errno -2] Name or service not known)) | 0.01 s |
| Self-signed HTTPS certificate | SSLError | SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate (_ssl.c:1029)') | 0.03 s |
| Cert trusted, but wrong host name | SSLError | ... Hostname mismatch, certificate is not valid for 'localhost'. | 0.00 s |
Server accepts but never answers, timeout=(3, 1) | ReadTimeout | No "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 reply | ConnectionError | No "Max retries" text: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response')) | 0.00 s |
Server never answers, no timeout | none | Still waiting when we killed it at 3 s | forever |
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
- Connection refused: the host is up but nothing listens on that port. Check the port, that the service is running and that it binds the right address. See ERR_CONNECTION_REFUSED / ECONNREFUSED.
- NameResolutionError: DNS. Check the spelling, a missing scheme or stray path in the host, and DNS inside containers. See ERR_NAME_NOT_RESOLVED.
- SSLError CERTIFICATE_VERIFY_FAILED: pass the CA file with
verify="/path/ca.pem"instead ofverify=False. The certificate must also match the host name you call. Our cert hadCN=localhostand no SAN, and Python rejected it forlocalhostwith "Hostname mismatch". Python checks the SAN only. See self-signed certificate in certificate chain. - RemoteDisconnected: the server or a proxy dropped the connection. Look at its logs. Often it crashed, or a keep-alive connection was closed while idle. See ERR_EMPTY_RESPONSE.
- ReadTimeout: the server is slow. Raise the read timeout or fix the slow endpoint.
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 behaviour | Server hits | Gaps between hits | Total time | Final exception |
|---|---|---|---|---|
| Closes without reply | 4 | 0.0, 1.0, 2.0 s | 3.01 s | ConnectionError ... Caused by ProtocolError('Connection aborted.', RemoteDisconnected(...)) |
| Always 503 | 4 | 0.0, 1.0, 2.0 s | 3.01 s | RetryError ... Caused by ResponseError('too many 503 error responses') |
| Never answers, read timeout 1 s | 4 | 1.0, 2.0, 3.0 s (1 s timeout + backoff) | 7.01 s | ConnectionError ... Caused by ReadTimeoutError(... Read timed out. (read timeout=1)) |
| Port closed (refused) | 0 | - | 3.00 s | ConnectionError ... Caused by NewConnectionError(... Connection refused) |
Things to notice:
total=3means 1 try plus 3 retries, so 4 hits.- With
backoff_factor=0.5the waits were 0, 1 and 2 s in our run. - After retries run out, a read timeout comes back as
ConnectionError, notReadTimeout. Code that only catchesTimeoutwill miss it. - Exhausted status retries raise
RetryError, which is not aConnectionError. - Without the adapter (default, 0 retries) a dropped connection fails at once with
('Connection aborted.', RemoteDisconnected(...))and 1 server hit.
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.