Programming languages
requests SSLError "Max retries exceeded with url": causes
requests.exceptions.SSLError: HTTPSConnectionPool(host='example.com', port=443): Max retries exceeded with urlrequests.exceptions.SSLError: HTTPSConnectionPool(host='example.com', port=443): Max retries exceeded with url means the TLS handshake failed and requests gave up. "Max retries" is misleading: requests does not retry by default. The real cause is the (Caused by SSLError(...)) text at the end of the line.
What the error means
The Caused by part names the TLS error; "Max retries exceeded" is only the urllib3 wrapper. requests hands every request to urllib3. When urllib3 cannot open the TLS connection, it raises MaxRetryError, whose message is always Max retries exceeded with url: <path> (Caused by <reason>), prefixed with the connection pool, for example HTTPSConnectionPool(host='example.com', port=443). requests then re-raises it as requests.exceptions.SSLError when the reason is an SSL error. The default HTTPAdapter uses Retry(0, read=False), so "max retries" here means zero retries were allowed and the first attempt failed.
Look at the end of the full message. These are the shapes you will usually see (the _ssl.c line number varies by Python version):
(Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1006)')))
(Caused by SSLError(SSLCertVerificationError(1, "[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: Hostname mismatch, certificate is not valid for 'example.com'. (_ssl.c:1006)")))
(Caused by SSLError(SSLEOFError(8, '[SSL: UNEXPECTED_EOF_WHILE_READING] EOF occurred in violation of protocol (_ssl.c:1006)')))
(Caused by SSLError(SSLError(1, '[SSL: WRONG_VERSION_NUMBER] wrong version number (_ssl.c:1006)')))
The bracketed part, such as CERTIFICATE_VERIFY_FAILED or WRONG_VERSION_NUMBER, is the OpenSSL reason code. Search for that code; it tells you which of the causes below applies.
Causes, most common first
1. Certificate verification failed
CERTIFICATE_VERIFY_FAILED with unable to get local issuer certificate or self-signed certificate in certificate chain is the most frequent reason. Either the server does not send its intermediate certificate, or the certificate is issued by a CA that your environment does not trust: an internal CA, a TLS-inspecting proxy, or a public root newer than your certifi release.
- Confirm:
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/nullshows onlydepth=0, or a verify code of 20 or 21. Paste the host into the SSL checker to see the chain from the outside. - Fix: if the chain is incomplete, the server must send the full chain (see missing intermediate certificate). If the issuer is a private or proxy CA, give requests a bundle that contains it. The full walkthrough is on Python CERTIFICATE_VERIFY_FAILED.
2. Hostname mismatch
Hostname mismatch, certificate is not valid for 'example.com' means the chain is trusted but the certificate does not list the name you requested in its Subject Alternative Name extension. Typical triggers are calling the bare domain when the certificate only covers www, or calling an IP address.
- Confirm:
openssl s_client -connect example.com:443 -servername example.com </dev/null | openssl x509 -noout -ext subjectAltName. - Fix: request a name that the certificate covers, or reissue the certificate with the missing name.
3. The proxy settings are wrong
WRONG_VERSION_NUMBER usually means TLS bytes were sent to something that answered in plain HTTP. With requests this is most often an HTTPS_PROXY value written as https://proxy.example.com:3128 for a proxy that only speaks HTTP, or a request sent to an HTTP port.
- Confirm:
env | grep -i proxyand compare with what the proxy supports. - Fix: use
http://in the proxy URL for an ordinary HTTP CONNECT proxy. Details: Python SSL WRONG_VERSION_NUMBER.
4. The connection was closed during the handshake
SSLEOFError / UNEXPECTED_EOF_WHILE_READING means the other side or something in between closed the TCP connection without a TLS alert. Firewalls, rate limiting, overloaded servers and servers that require SNI or a newer protocol are typical. See EOF occurred in violation of protocol and unexpected eof while reading.
5. Old or weak server TLS
OpenSSL 3 rejects some legacy servers outright. The reason code names the problem: UNSAFE_LEGACY_RENEGOTIATION_DISABLED for servers without RFC 5746 support, DH_KEY_TOO_SMALL for weak DHE parameters. These are server-side problems that should be fixed on the server.
Fix commands by platform
Give requests the right CA bundle instead of disabling verification. requests reads REQUESTS_CA_BUNDLE, then CURL_CA_BUNDLE, when verify is left at its default.
Debian / Ubuntu / Docker images:
apt-get update && apt-get install -y ca-certificates
cp company-root.crt /usr/local/share/ca-certificates/company-root.crt
update-ca-certificates
export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
RHEL, Rocky, Alma:
cp company-root.crt /etc/pki/ca-trust/source/anchors/
update-ca-trust
export REQUESTS_CA_BUNDLE=/etc/pki/tls/certs/ca-bundle.crt
macOS and Windows: use the operating system trust store through truststore (Python 3.10 or later), which also picks up roots deployed by your IT department:
import truststore
truststore.inject_into_ssl() # call once, before creating sessions
import requests
requests.get("https://example.com")
Per call, in code:
import requests
requests.get("https://intranet.example.com", verify="/etc/ssl/certs/company-bundle.pem")
Don't disable certificate verification
Disabling verification hides the error and exposes the traffic. verify=False turns the error into an InsecureRequestWarning and removes the check that proves you are talking to the real server. Anyone on the network path, including a compromised Wi-Fi router or proxy, can then read or change the traffic, including API keys, cookies and passwords. Monkeypatching ssl._create_default_https_context does the same for every library in the process.
Safer alternatives cover every legitimate case: pass verify="/path/to/bundle.pem" for a private CA, set REQUESTS_CA_BUNDLE for a whole environment, use truststore when the root is already in the operating system store, and fix the server chain when an intermediate is missing. For a short local test against a self-signed server, trust that one certificate file rather than turning verification off.
Verify the fix
python3 - <<'EOF'
import requests
r = requests.get("https://example.com", timeout=10)
print(r.status_code, r.url)
EOF
The script must print a status code without raising SSLError and without an InsecureRequestWarning. Also confirm the bundle path you set exists (ls -l "$REQUESTS_CA_BUNDLE"), because a wrong path produces a different error that is easy to confuse with this one. Finally, re-check the host in the SSL checker so you know the server side is clean, and read certificate chains explained if you need to understand why one missing intermediate breaks Python but not your browser.
FAQ
Will adding retries with a Retry object or HTTPAdapter(max_retries=...) fix this?
No. Retries help with flaky networks, not with TLS failures that happen the same way every time. A missing intermediate certificate, an untrusted root or a proxy speaking plain HTTP fails identically on every attempt. Fix the cause shown after Caused by first; add retries only for genuinely intermittent errors such as timeouts.
Why does the same URL work in curl or my browser?
Each client uses its own trust store. Browsers use the operating system store and can fetch a missing intermediate; curl uses the system bundle it was built with; requests uses certifi unless told otherwise. Check the server chain with the chain builder. If the chain is complete, point requests at the bundle the other tools are using.
Is it a problem with my code or with the server?
Run openssl s_client -connect example.com:443 -servername example.com from the same machine. If it reports Verify return code: 0 (ok) the server is fine and your Python environment needs a CA bundle; if it reports 20 or 21, the server is not sending a complete chain and only the server owner can fix that properly.
Does upgrading certifi help?
Only when the server chains to a public root that your old certifi release does not contain yet. It does not help with internal CAs, TLS-inspecting proxies, missing intermediates or handshake failures that have nothing to do with certificates.