Requests timeout vs connect timeout: clarification and best practices
Unexpected latency in Python’s requests library often shows up in production microservices that call external APIs, where the connection establishment stalls but the overall request seems to hang. This happens because the timeout argument can control both the socket connect phase and the data read phase separately. Mis‑configuring them can silently delay retries and inflate latency.
# Example showing the issue
import requests, time
url = 'https://httpbin.org/delay/10' # endpoint waits 10 s before responding
start = time.time()
try:
# single float applies to both connect and read
r = requests.get(url, timeout=5)
print('status', r.status_code)
except requests.exceptions.Timeout:
print('Timeout after', round(time.time() - start, 2), 'seconds')
# Output: Timeout after 5.0 seconds
# Using a tuple isolates the connect phase (3 s) from the read phase (7 s)
start = time.time()
try:
r = requests.get(url, timeout=(3, 7))
print('status', r.status_code)
except requests.exceptions.ConnectTimeout:
print('Connect timeout after', round(time.time() - start, 2), 'seconds')
except requests.exceptions.ReadTimeout:
print('Read timeout after', round(time.time() - start, 2), 'seconds')
# Output: Read timeout after 7.0 seconds
A single numeric timeout applies to both the connection handshake and the subsequent data read, so a slow server can exhaust the total limit even if the TCP handshake is quick. Supplying a (connect, read) tuple lets you bound each phase independently. This behavior follows urllib3’s Timeout object and is documented by the requests library. Related factors:
- Network latency spikes during DNS/TCP handshake
- Large payloads that take long to stream
- Default timeout of None meaning no limit
To diagnose this in your code:
# Detect which phase timed out
import requests
try:
requests.get('https://example.com', timeout=(2, 4))
except requests.exceptions.ConnectTimeout:
print('Connection could not be established within 2 s')
except requests.exceptions.ReadTimeout:
print('Server did not send data within 4 s after connection')
Fixing the Issue
Quick fix: set a tuple so the two limits are explicit.
import requests
response = requests.get('https://api.service.com/data', timeout=(2, 5))
When the connect phase exceeds 2 seconds you get a ConnectTimeout; when the server stalls longer than 5 seconds you get a ReadTimeout.
Best‑practice (production‑ready): configure both timeouts, add retry logic, and log the exact failure.
import logging, time
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
logger = logging.getLogger(__name__)
session = requests.Session()
retry = Retry(total=3, backoff_factor=0.5,
status_forcelist=[502, 503, 504],
raise_on_status=False)
session.mount('https://', HTTPAdapter(max_retries=retry))
url = 'https://api.service.com/data'
timeout = (2, 5) # (connect, read) seconds
try:
start = time.time()
resp = session.get(url, timeout=timeout)
resp.raise_for_status()
logger.info('Request succeeded in %.2f s', time.time() - start)
except requests.exceptions.ConnectTimeout:
logger.error('Connect timeout after %.2f s', time.time() - start)
except requests.exceptions.ReadTimeout:
logger.error('Read timeout after %.2f s', time.time() - start)
except requests.exceptions.RequestException as exc:
logger.error('Request failed: %s', exc)
What Doesn’t Work
❌ Setting timeout=None to “disable” issues: This removes all limits and can hang your service indefinitely
❌ Catching only requests.exceptions.Timeout and retrying without inspecting the exception: You may retry on a ConnectTimeout that will fail again, wasting resources
❌ Using time.sleep() before each request to avoid timeouts: This only masks the problem and inflates overall latency
- Using a single float timeout and assuming it only limits connection time
- Swapping the tuple order (read, connect) which leads to unexpected early failures
- Ignoring the specific ConnectTimeout/ReadTimeout exceptions and handling only generic Timeout
When NOT to optimize
- One‑off scripts: A quick single‑float timeout is fine for ad‑hoc data pulls.
- Trusted internal services: If the network is controlled and latency is negligible, extra granularity adds little value.
- Prototyping in notebooks: Simpler code speeds iteration; detailed retries can be added later.
- Very short‑lived CLI tools: Over‑engineering timeout handling may outweigh benefits.
Frequently Asked Questions
Q: Can I set only a connect timeout without affecting the read timeout?
Yes, pass a tuple (connect, None) or (connect, read) to control each phase separately.
Q: Why does a request sometimes take longer than the timeout value I set?
If you use a single float, the value limits the total time, not each phase; a slow read can exceed the limit after the connect succeeded.
Understanding the distinction between connect and read timeouts is essential for reliable API integration. By explicitly configuring each phase and adding structured retry handling, you prevent hidden hangs and keep latency predictable in production services.