Why This Error Happens
If you've spent any time scraping web data or calling APIs with Python, you've likely hit this wall. This error isn't just one problem; it is a signal that your requests call failed to establish a handshake with the server after several attempts.
By default, the library tries to connect immediately. If the network jitters or the server is busy for even 100 milliseconds, the connection fails. When these failures hit a predefined limit—the "Max retries"—Python gives up and throws this exception to prevent your script from looping forever.
Common Root Causes
- Aggressive Rate Limiting: APIs like GitHub or Twitter often limit users to 5,000 requests per hour. If you exceed this, the server may drop your connection entirely.
- DNS Resolution Failures: Your local machine cannot translate the domain name into an IP address, often due to a misconfigured
/etc/hostsfile or a slow DNS provider like 8.8.8.8. - Network Instability: High packet loss on a public Wi-Fi or a flickering VPN connection can terminate a TCP handshake mid-way.
- WAF Blocking: Security layers like Cloudflare might see the default Python header and flag your traffic as a bot.
- Socket Exhaustion: Opening 1,000 individual connections in a
forloop without closing them will eventually run your OS out of available ports.
Fix 1: Use Exponential Backoff
The smartest fix is to tell Python to be patient. Instead of failing immediately, use an HTTPAdapter to retry the request with increasing wait times. This approach handles 502 or 504 errors gracefully.
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
def fetch_data(url):
session = requests.Session()
# Setting total=5 and backoff_factor=1 means Python waits
# 1s, 2s, 4s, 8s, then 16s between attempts.
retry_strategy = Retry(
total=5,
backoff_factor=1,
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["HEAD", "GET", "OPTIONS"]
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("https://", adapter)
session.mount("http://", adapter)
try:
response = session.get(url, timeout=10)
response.raise_for_status()
return response.json()
except requests.exceptions.ConnectionError as ce:
print(f"Connection failed: {ce}")
except requests.exceptions.Timeout:
print("The server took too long to respond.")
except requests.exceptions.RequestException as e:
print(f"A general error occurred: {e}")
# Example usage
data = fetch_data("https://api.example.com/v1/resource")
Fix 2: Mimic a Real Browser
Many servers automatically reject requests that identify as python-requests/2.28.1. You can bypass these basic filters by adding a User-Agent header that looks like a standard Chrome or Firefox browser.
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36'
}
# This makes the request look like it is coming from a Windows 10 desktop
response = requests.get("https://api.example.com/data", headers=headers)
Fix 3: Reuse Connections with Sessions
Creating a new connection for every single request is expensive and prone to failure. Use a requests.Session() object. This keeps the underlying TCP connection open for multiple requests to the same host, which significantly reduces the chance of a handshake error.
# Using a context manager handles closing the session automatically
with requests.Session() as session:
for item_id in range(1, 101):
try:
# Reuses the same connection pool for all 100 requests
resp = session.get(f"https://api.example.com/items/{item_id}", timeout=5)
process_data(resp.json())
except requests.exceptions.ConnectionError:
print(f"Skipping item {item_id} due to connection failure.")
continue
Fix 4: Configure Proxy Settings
In a corporate environment, your traffic might be blocked by a firewall unless you route it through a specific proxy. You can define these settings globally in your script to avoid connection drops.
proxies = {
'http': 'http://proxy.company.com:8080',
'https': 'http://proxy.company.com:1080',
}
requests.get("https://api.example.com", proxies=proxies)
Pro-Tip: Check Your Subnet
Sometimes the issue isn't your code, but a routing conflict. When I troubleshoot persistent drops in local dev environments, I use a Subnet Calculator. This helps verify if the API endpoint resides on a restricted subnet that requires a specific gateway or VPN route to reach.
How to Verify the Fix
Don't just hope it works; look at the logs. You can enable deep debugging to see every connection attempt and retry in real-time.
import logging
import http.client as http_client
# Turn on verbose debugging for the underlying urllib3 library
http_client.HTTPConnection.debuglevel = 1
logging.basicConfig()
logging.getLogger().setLevel(logging.DEBUG)
requests_log = logging.getLogger("urllib3")
requests_log.setLevel(logging.DEBUG)
requests_log.propagate = True
Watch the output. If you see "Retrying (Retry(total=4...))", your backoff strategy is doing its job. If the script succeeds on the third try, you've successfully mitigated a transient network issue.
Summary Checklist
- Set a timeout: Always use
timeout=10(or similar) to prevent your script from hanging for minutes. - Close your sessions: Use context managers (
with) to free up system resources. - Respect the server: If you get 429 errors, increase your
backoff_factorto 2 or 3 to give the server more breathing room.

