# requests SSLError "Max retries exceeded with url": causes

> requests raises SSLError: Max retries exceeded when the TLS handshake fails. Read the Caused by part and fix the chain, CA bundle or proxy.

Source: https://ssl.sh/errors/python-requests-max-retries-ssl-error/
Last updated: 2026-11-10

**`requests.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):

```text
(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/null` shows only `depth=0`, or a verify code of 20 or 21. Paste the host into the [SSL checker](https://ssl.sh/tools/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](https://ssl.sh/errors/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](https://ssl.sh/errors/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 proxy` and 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](https://ssl.sh/errors/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](https://ssl.sh/errors/python-ssl-eof-occurred-in-violation-of-protocol/) and [unexpected eof while reading](https://ssl.sh/errors/openssl-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](https://ssl.sh/errors/python-unsafe-legacy-renegotiation-disabled/) for servers without RFC 5746 support, [DH_KEY_TOO_SMALL](https://ssl.sh/errors/openssl-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:**

```bash
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:**

```bash
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:

```python
import truststore
truststore.inject_into_ssl()   # call once, before creating sessions

import requests
requests.get("https://example.com")
```

**Per call, in code:**

```python
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

```bash
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](https://ssl.sh/tools/ssl-checker/) so you know the server side is clean, and read [certificate chains explained](https://ssl.sh/learn/certificate-chains/) if you need to understand why one missing intermediate breaks Python but not your browser.
