Python Requests/urllib3: Resolving Persistent Timeout and ReadTimeout Errors
This error occurs when Python sends HTTP requests that exceed socket connect or read time limits. Specify an explicit timeout tuple timeout=(connect_timeout, read_timeout) such as timeout=(3.05, 30.0).
Root Cause Analysis
This error occurs when Python code making HTTP calls via requests or urllib3 exceeds the allocated time limit waiting for the server to accept the connection or send response bytes, raising requests.exceptions.ConnectTimeout, requests.exceptions.ReadTimeout, or a standard TimeoutError.
Root Cause 1: Conflating Connect Timeout and Read Timeout
Passing a single float like timeout=5.0 applies 5 seconds to BOTH the initial TCP connection handshake and the time between individual bytes received. If a remote server accepts the connection immediately (0.1s) but takes 6 seconds to execute a database query before streaming data, timeout=5.0 aborts with a ReadTimeout.
Root Cause 2: Slow Remote Backend Database Queries or Heavy Payloads
Endpoints generating large CSV exports or running complex analytics take significant time to produce the first byte.
Root Cause 3: Network Packet Drops and TCP Retransmissions
On unstable network connections, packet loss causes TCP backoff, exceeding small hardcoded timeout values.
Root Cause 4: Thread or Event Loop Starvation
In high-concurrency applications, CPU saturation delays socket processing, prompting internal timers to trigger timeout exceptions prematurely.
Reproduction Code (MCVE)
# Simulating socket timeout when reading from slow endpoint
def execute_http_request(timeout_tuple: tuple):
connect_t, read_t = timeout_tuple
# If read timeout is impractically low (< 0.05s), simulate socket timeout
if read_t < 0.05:
raise TimeoutError(
f"HTTPSConnectionPool: Read timed out. (read timeout={read_t}). "
"Server took longer to send response bytes than allotted read timeout."
)
return {"status_code": 200, "data": "OK"}
# Executing request with insufficient read timeout triggers TimeoutError
execute_http_request((3.05, 0.01))
Solution 1: Pass Explicit Connect and Read Timeout Tuples
Provide a two-element tuple (connect_timeout, read_timeout) to allocate a short connection window and an adequate read duration.
import requests
# Solution 1: Recommended timeout tuple configuration
# Connect timeout: 3.05s (slightly greater than a multiple of 3 for TCP SYN retries)
# Read timeout: 30.0s (gives remote server adequate time to process complex requests)
recommended_timeout = (3.05, 30.0)
def safe_api_call_simulation(timeout_config: tuple):
assert len(timeout_config) == 2
assert timeout_config[0] > 3.0
assert timeout_config[1] >= 10.0
return {"status": "configured_properly", "timeout": timeout_config}
result = safe_api_call_simulation(recommended_timeout)
print("Timeout configuration validated:", result)
assert result["status"] == "configured_properly"
Solution 2: Implement Exponential Backoff Retries on Timeout
Use urllib3.util.retry.Retry to retry requests that encounter transient read timeouts.
from urllib3.util.retry import Retry
from requests.adapters import HTTPAdapter
import requests
# Solution 2: Retrying on timeouts
session = requests.Session()
retries = Retry(
total=3,
backoff_factor=2,
read=3, # Retry on read timeouts
connect=3, # Retry on connect timeouts
status_forcelist=[504]
)
adapter = HTTPAdapter(max_retries=retries)
session.mount("https://", adapter)
print("Session retry adapter mounted successfully.")
assert adapter.max_retries.read == 3
Never set timeout=None in production unless you are intentionally maintaining a permanent streaming connection (such as Server-Sent Events / SSE). Setting timeout=None means Python will hang forever if the remote server drops the connection without closing the socket.
Contrast ConnectTimeout with ReadTimeout: ConnectTimeout happens during the initial TCP/TLS handshake before the server receives the request; ReadTimeout happens after the server receives the request but delays transmitting the response bytes.