InterviewsVector

Fix “Max Retries Exceeded with URL” in Python Requests

Quick answer

Max retries exceeded with URL is usually a requests.ConnectionError wrapping urllib3's MaxRetryError. Read the final Caused by message: fix DNS, refused connections, routing, TLS, or proxy configuration first. Add bounded exponential-backoff retries only for transient failures and operations that are safe to repeat.

The exception usually looks like this:

requests.exceptions.ConnectionError:
HTTPSConnectionPool(host='api.example.com', port=443):
Max retries exceeded with url: /v1/items
(Caused by NewConnectionError(...))

Max retries exceeded with URL is a summary, not the root cause. Read the final Caused by ... text first. It normally tells you whether the failure happened during DNS lookup, TCP connection, TLS verification, proxy negotiation, or a retry policy you explicitly configured.

Important: Requests does not retry failed connections by default. Its adapter uses urllib3 internally, which can still raise an exception named MaxRetryError after the initial connection attempt exhausts the configured allowance. Do not assume the fix is simply to increase the retry count.

Diagnose the nested cause first

Nested message or exceptionWhat it usually meansFirst corrective action
NameResolutionError, getaddrinfo failed, or Temporary failure in name resolutionDNS could not resolve the hostnameVerify the hostname and resolve it from the same machine, container, or serverless runtime
Connection refused, Errno 111, or WinError 10061The host responded, but no service accepted the target portStart the service; verify port, scheme, bind address, and container mapping
No route to host or Network is unreachableRouting, VPN, local-network permission, firewall, or network policy blocked the pathTest reachability from the same runtime and inspect network rules
ConnectTimeoutErrorThe TCP or TLS connection was not established before the connect timeoutCheck routing, firewall, server load, and the connect-timeout value
SSLCertVerificationError or SSLErrorCertificate chain, hostname, expiry, protocol, or intercepting proxy problemRepair trust configuration; do not disable verification
ProxyErrorThe configured proxy could not be reached or usedCheck proxy URL, credentials, environment variables, and NO_PROXY
ResponseError: too many 5xx responsesA custom retry policy exhausted status-code retriesInspect server health and retry policy; respect Retry-After where applicable
Troubleshooting flowchart for Python Requests Max retries exceeded with URL errors using the nested cause to distinguish DNS, refused connection, timeout, TLS, proxy, and HTTP status failures

Fix deterministic connection or configuration failures before adding a retry policy for transient failures.

Step 1: verify the URL and the failing environment

Use a complete URL with the correct scheme, hostname, and port:

from urllib.parse import urlsplit
 
url = "https://api.example.com:443/v1/items"
parts = urlsplit(url)
 
if parts.scheme not in {"http", "https"} or not parts.hostname:
    raise ValueError(f"Invalid HTTP URL: {url!r}")

Test from the same place where Python fails. Your laptop, Docker container, CI runner, Kubernetes pod, and cloud function may use different DNS servers, routes, proxies, or certificate bundles.

# Resolve the hostname using the runtime's DNS configuration.
python -c "import socket; print(socket.getaddrinfo('api.example.com', 443))"
 
# Inspect DNS, TCP, TLS, redirects, and response headers.
curl -v --connect-timeout 5 https://api.example.com/v1/items

Do not rely only on ping; many valid HTTP services block ICMP traffic.

Step 2: set connect and read timeouts

Nearly all production Requests calls should set a timeout. A tuple separates the connection phase from response inactivity:

import requests
 
response = requests.get(
    "https://api.example.com/v1/items",
    timeout=(3.05, 30),  # connect timeout, read timeout
)
response.raise_for_status()

The read timeout is not a deadline for downloading the entire response. Requests raises it when no bytes arrive on the socket for the configured period. A timeout prevents indefinite waiting; it does not fix DNS, routing, or certificate configuration.

Step 3: handle the correct exception type

Catch specific exceptions before their broader parent classes. SSLError and ProxyError, for example, are connection errors:

import requests
 
url = "https://api.example.com/v1/items"
 
try:
    response = requests.get(url, timeout=(3.05, 30))
    response.raise_for_status()
except requests.exceptions.SSLError as exc:
    print(f"TLS verification failed: {exc}")
except requests.exceptions.ProxyError as exc:
    print(f"Proxy connection failed: {exc}")
except requests.exceptions.ConnectTimeout as exc:
    print(f"Connection timed out: {exc}")
except requests.exceptions.ReadTimeout as exc:
    print(f"Server stopped sending data: {exc}")
except requests.exceptions.ConnectionError as exc:
    print(f"DNS, routing, or TCP connection failed: {exc}")
except requests.exceptions.HTTPError as exc:
    print(f"HTTP error response: {exc.response.status_code}")

In a service, log structured fields such as hostname, exception class, attempt number, elapsed time, and status code. Do not log credentials, authorization headers, or sensitive query parameters.

Fix DNS resolution failures

For NameResolutionError or getaddrinfo failed:

  1. Check the hostname for spelling mistakes and unintended whitespace.
  2. Confirm the hostname resolves inside the failing container or runtime.
  3. Check VPN, split-DNS, Kubernetes DNS, and /etc/resolv.conf where relevant.
  4. Verify that an HTTP proxy is not expected to perform DNS on your behalf.
  5. Treat a consistently nonexistent hostname as a configuration error, not a retryable outage.

Short retries can help a genuinely transient resolver failure, but repeated DNS attempts cannot repair a bad hostname.

Fix connection refused or no route to host

Connection refused normally means the network reached the host but no process accepted the requested port. Verify:

  • The server process is running and listening on the expected port.
  • The request uses the correct http:// or https:// scheme.
  • A local service binds to an accessible interface rather than only 127.0.0.1.
  • Docker or Kubernetes exposes and maps the port correctly.
  • Security groups, firewalls, network policies, and local-network permissions allow the path.

No route to host points earlier in the network path: routing, VPN, subnet, firewall, or host availability. Increasing max_retries only repeats the failed route.

Fix TLS certificate failures safely

Requests verifies HTTPS certificates by default. Keep that protection enabled.

For a private organizational CA, provide the trusted CA bundle:

response = requests.get(
    "https://internal.example.com/health",
    verify="/etc/ssl/certs/company-ca.pem",
    timeout=(3.05, 30),
)

Or configure it for the process:

export REQUESTS_CA_BUNDLE=/etc/ssl/certs/company-ca.pem

Also check certificate expiry, hostname mismatch, missing intermediate certificates, system time, and TLS interception by corporate proxies. verify=False suppresses verification and makes the connection vulnerable to man-in-the-middle attacks; it is not a production fix.

Check proxy settings

Requests can read HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, and NO_PROXY from the environment. An obsolete or malformed proxy can cause every request to fail.

import requests
 
url = "https://api.example.com/v1/items"
print(requests.utils.get_environ_proxies(url))

Do not print this mapping in shared logs if proxy URLs contain credentials. If environment proxies are unintended for a controlled runtime, disable their use explicitly:

session = requests.Session()
session.trust_env = False
response = session.get(url, timeout=(3.05, 30))

Use this only when bypassing the environment proxy is allowed by your network policy.

Add retries only for transient failures

After fixing deterministic configuration problems, use a Session and HTTPAdapter for bounded retry behavior:

import requests
from requests.adapters import HTTPAdapter
from urllib3.util import Retry
 
 
def build_retrying_session() -> requests.Session:
    retry = Retry(
        total=5,
        connect=3,
        read=2,
        status=3,
        redirect=3,
        other=0,
        backoff_factor=0.5,
        status_forcelist=(429, 500, 502, 503, 504),
        allowed_methods=frozenset({"HEAD", "GET", "OPTIONS"}),
        respect_retry_after_header=True,
        raise_on_status=False,
    )
 
    adapter = HTTPAdapter(max_retries=retry)
    session = requests.Session()
    session.mount("https://", adapter)
    session.mount("http://", adapter)
    return session
 
 
with build_retrying_session() as session:
    response = session.get(
        "https://api.example.com/v1/items",
        timeout=(3.05, 30),
    )
    response.raise_for_status()

This example:

  • Limits total and category-specific attempts.
  • Uses exponential backoff instead of immediate retry loops.
  • Retries common transient statuses, including 429 Too Many Requests.
  • Honors Retry-After where urllib3 supports it.
  • Restricts automatic retries to methods that should not create duplicate side effects.
  • Returns the final HTTP response after status retries are exhausted, allowing raise_for_status() to produce an HTTPError with response context.

Choose counts and timeouts from your latency budget. Five attempts with long timeouts can turn a short upstream failure into a very slow user request.

Be careful when retrying POST, PUT, and DELETE

An HTTP operation can reach the server even if the client never receives the response. Retrying a payment, order, or message creation can therefore duplicate the side effect.

Retry POST only when the API supports an idempotency key or equivalent deduplication mechanism:

headers = {"Idempotency-Key": operation_id}
response = session.post(
    "https://api.example.com/v1/orders",
    json=payload,
    headers=headers,
    timeout=(3.05, 30),
)

The server—not only the client—must enforce the idempotency guarantee.

Enable connection diagnostics temporarily

urllib3 debug logs reveal connection creation, retry increments, and response status. Enable them only while diagnosing and review logs for sensitive URLs or headers:

import logging
 
logging.basicConfig(level=logging.INFO)
logging.getLogger("urllib3").setLevel(logging.DEBUG)

Pair the logs with request IDs and server-side traces when possible. Client retries can hide an unstable dependency unless retry counts and final outcomes are observable.

Common fixes that make the problem worse

  • Blindly increasing retries: delays deterministic DNS, port, proxy, and certificate failures.
  • Removing the timeout: lets the program wait indefinitely during network failure.
  • Setting verify=False: hides TLS configuration errors and removes certificate protection.
  • Retrying every exception: repeats programming errors and non-transient failures.
  • Retrying every HTTP method: can duplicate payments, writes, emails, and other side effects.
  • Using requests.packages.urllib3: import Retry from the public urllib3.util module instead of Requests' historical vendored namespace.
  • Catching only Exception: loses the distinction between DNS, timeout, TLS, proxy, and HTTP errors.

Authoritative references

This article was technically reviewed against the Requests and urllib3 documentation on August 2, 2026.

Key takeaways

  • The words max retries do not prove that Requests retried several times; Requests does not retry failed connections by default.
  • The final Caused by line is the useful diagnostic signal: it distinguishes DNS failure, connection refused, timeout, TLS failure, proxy failure, and exhausted status retries.
  • Set explicit connect and read timeouts on production requests; a timeout is not the same thing as a retry.
  • Retry transient failures with bounded exponential backoff, Retry-After support, and a narrow set of safe HTTP methods.
  • Do not disable TLS verification or blindly retry POST requests to make the exception disappear.

Frequently asked questions

What does "Max retries exceeded with URL" mean in Python Requests?

Requests could not complete the HTTP operation through its urllib3 connection pool. The message is normally wrapped in requests.exceptions.ConnectionError. The nested cause identifies whether DNS, TCP connection, routing, TLS, proxy configuration, or an explicitly configured status retry failed.

Why do I see MaxRetryError when Requests defaults to zero retries?

Requests uses urllib3 internally, and urllib3 reports MaxRetryError when the allowed connection attempts are exhausted—even when the configured retry count is zero and only the initial attempt occurred. The exception name alone is not evidence of multiple retries.

Should I increase max_retries to fix the error?

Only for transient failures such as connect timeouts, rate limits, or temporary 5xx responses. Extra retries do not repair a misspelled hostname, closed port, invalid certificate, or broken proxy, and they can make failures slower or amplify load.

How do I fix NameResolutionError or getaddrinfo failed?

Verify the hostname and scheme, test DNS resolution from the same runtime environment, and check VPN, container, DNS, and proxy settings. Retries may help a brief resolver outage but will not fix a consistently invalid hostname.

How do I fix Connection refused in Python Requests?

Confirm the service is running and listening on the requested interface and port, verify HTTP versus HTTPS, and check container port mappings and firewall rules. Connection refused normally means the host was reached but nothing accepted the TCP connection.

Can I fix an SSL error with verify=False?

Do not use verify=False as a production fix. It disables certificate and hostname verification and enables man-in-the-middle attacks. Correct the hostname or certificate chain, update the CA bundle, or configure REQUESTS_CA_BUNDLE with the trusted organizational CA.

Which HTTP methods are safe to retry?

GET, HEAD, and OPTIONS are normally safe starting points. Retry PUT or DELETE only when the API's idempotency guarantees are understood. Retry POST only when the server supports an idempotency key or another mechanism that prevents duplicate side effects.

Does the Requests timeout limit the whole download?

No. Requests documents timeout as socket inactivity thresholds, not a total wall-clock deadline for the entire response. A tuple can set separate connect and read timeouts, such as timeout=(3.05, 30).

By Mohammad Wasi

Software Engineering Leader & Technical Author · Updated August 2, 2026


Related Posts