requests.exceptions.SSLError: HTTPSConnectionPool Max retries exceeded (tested)

requests.exceptions.SSLError: HTTPSConnectionPool(...): Max retries exceeded with url ... (Caused by SSLError(...)) is a TLS failure, not a retry problem. The reason inside tells you the fix. We reproduced four reasons against local servers and tested verify=, REQUESTS_CA_BUNDLE and verify=False.

Setup: a test root CA and three server certificates made with openssl (self-signed, signed by the private CA, and one issued for the wrong hostname), served by this helper on localhost ports 8963-8966:

import http.server, ssl, threading

class Quiet(http.server.SimpleHTTPRequestHandler):
    def log_message(self, *a): pass

def serve(certfile, port, tls=True):
    srv = http.server.HTTPServer(("localhost", port), Quiet)
    if tls:
        ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
        ctx.load_cert_chain(certfile)
        srv.socket = ctx.wrap_socket(srv.socket, server_side=True)
    threading.Thread(target=srv.serve_forever, daemon=True).start()
    return srv

1. The error

import requests
from tlsserver import serve

serve("selfsigned.pem", 8963)
try:
    requests.get("https://localhost:8963/", timeout=5)
except Exception as e:
    print(f"{type(e).__module__}.{type(e).__name__}: {e}")

Output:

requests.exceptions.SSLError: HTTPSConnectionPool(host='localhost', port=8963): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate (_ssl.c:1029)')))

requests wraps the real error twice: urllib3 retries the connection, gives up with Max retries exceeded, and the actual cause is inside Caused by SSLError(...). Ignore the HTTPSConnectionPool and retries text; the reason after certificate verify failed: tells you which fix you need.

2. Four causes, four different reasons

import requests
from tlsserver import serve

cases = {"self-signed": ("selfsigned.pem", True, True),
         "private CA": ("leaf.pem", True, True),
         "wrong hostname (CA trusted)": ("wrongname.pem", True, "ca.pem"),
         "plain HTTP port": (None, False, True)}
for port, (label, (cert, tls, verify)) in enumerate(cases.items(), start=8963):
    serve(cert, port, tls=tls)
    try:
        requests.get(f"https://localhost:{port}/", verify=verify, timeout=5)
    except requests.exceptions.SSLError as e:
        print(f"{label}:\n  {e}\n")

Output:

self-signed:
  HTTPSConnectionPool(host='localhost', port=8963): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate (_ssl.c:1029)')))

private CA:
  HTTPSConnectionPool(host='localhost', port=8964): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1029)')))

wrong hostname (CA trusted):
  HTTPSConnectionPool(host='localhost', port=8965): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, "[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: Hostname mismatch, certificate is not valid for 'localhost'. (_ssl.c:1029)")))

plain HTTP port:
  HTTPSConnectionPool(host='localhost', port=8966): Max retries exceeded with url: / (Caused by SSLError(SSLError(1, '[SSL: WRONG_VERSION_NUMBER] wrong version number (_ssl.c:1029)')))

3. Get the reason in code

import requests
from tlsserver import serve

serve("leaf.pem", 8963)
try:
    requests.get("https://localhost:8963/", timeout=5)
except requests.exceptions.SSLError as e:
    reason = e.args[0].reason          # urllib3's MaxRetryError -> the underlying SSL error
    print("reason:", reason.args[0])
    print("is ConnectionError:", isinstance(e, requests.exceptions.ConnectionError))

Output:

reason: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1029)
is ConnectionError: True

requests.exceptions.SSLError is a subclass of ConnectionError, so except requests.exceptions.ConnectionError also catches it. Put the SSLError clause first if you want to handle it separately.

4. Fix: tell requests which CA to trust

import requests
from tlsserver import serve

serve("leaf.pem", 8963)                       # cert signed by our private CA
r = requests.get("https://localhost:8963/", verify="ca.pem", timeout=5)
print("verify='ca.pem':", r.status_code)

s = requests.Session()
s.verify = "ca.pem"                           # applies to every request in the session
print("session.verify:", s.get("https://localhost:8963/", timeout=5).status_code)

Output:

verify='ca.pem': 200
session.verify: 200
import os
os.environ["REQUESTS_CA_BUNDLE"] = "ca.pem"   # or export it in the shell
import requests
from tlsserver import serve

serve("leaf.pem", 8963)
print("REQUESTS_CA_BUNDLE:", requests.get("https://localhost:8963/", timeout=5).status_code)

Output:

REQUESTS_CA_BUNDLE: 200

verify= takes a path to a PEM bundle (or a directory prepared with c_rehash). REQUESTS_CA_BUNDLE sets the same thing for every request without code changes. Note that this replaces the default bundle: if you point it at only your private CA, public sites will fail. Append your CA to a copy of the default bundle if you need both.

5. Which bundle requests uses by default

import requests, certifi
print("requests", requests.__version__)
print("default bundle:", requests.utils.DEFAULT_CA_BUNDLE_PATH)
print("certifi.where():", certifi.where())

Output:

requests 2.32.3
default bundle: /etc/ssl/certs/ca-certificates.crt
certifi.where(): /etc/ssl/certs/ca-certificates.crt

That was Debian's packaged requests, whose certifi is patched to use the system store. With requests installed by pip (python:3-alpine, requests 2.34.2) it is certifi's own file:

requests 2.34.2
default bundle: /usr/local/lib/python3.14/site-packages/certifi/cacert.pem
certifi.where(): /usr/local/lib/python3.14/site-packages/certifi/cacert.pem

So adding a CA to the operating system trust store does not help a pip-installed requests; use verify= or REQUESTS_CA_BUNDLE. Upgrading certifi (pip install -U certifi) fixes errors caused by an outdated bundle.

6. verify=False works, but only as a diagnostic

import requests
from tlsserver import serve

serve("selfsigned.pem", 8963)
r = requests.get("https://localhost:8963/", verify=False, timeout=5)
print("verify=False:", r.status_code)

Output:

/usr/lib/python3/dist-packages/urllib3/connectionpool.py:1109: InsecureRequestWarning: Unverified HTTPS request is being made to host 'localhost'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
  warnings.warn(
verify=False: 200

It returns 200 and urllib3 prints InsecureRequestWarning on every request. It also disables the hostname check, so anyone in the path can read and change the traffic. Use it once to confirm the problem is the certificate, then fix the trust as in step 4. For the wrong-version case, switch the scheme instead:

import requests
from tlsserver import serve

serve(None, 8966, tls=False)                   # a server that only speaks plain HTTP
print("http://", requests.get("http://localhost:8966/", timeout=5).status_code)

Output:

http:// 200

Related: SSL: CERTIFICATE_VERIFY_FAILED, 5 causes · SSL: WRONG_VERSION_NUMBER · requests ConnectionError: Max retries exceeded.

Frequently asked questions

What causes requests SSLError HTTPSConnectionPool Max retries exceeded?

A TLS handshake failure. Read the reason inside Caused by SSLError: self-signed certificate, unable to get local issuer certificate, Hostname mismatch, or WRONG_VERSION_NUMBER.

How do I fix CERTIFICATE_VERIFY_FAILED in requests?

Pass verify='/path/to/ca.pem' (or set REQUESTS_CA_BUNDLE) pointing at the CA that signed the server certificate. Upgrade certifi if the bundle is outdated.

Is verify=False safe?

No. It disables certificate and hostname checks and prints InsecureRequestWarning. Use it only to confirm the problem, then fix the CA trust.

Written with AI assistance. Measured on 2026-10-23 with Python 3.13.5 and requests 2.32.3 (Debian) against local HTTPS servers using certificates we generated with openssl 3; the default-bundle check was repeated with requests 2.34.2 from pip in python:3-alpine. Each block was run verbatim and the output pasted unedited. PlainHeaders is our extension; requests is a free third-party library.