requests ReadTimeout: Read timed out (tested)
requests.exceptions.ReadTimeout: HTTPConnectionPool(...): Read timed out. (read timeout=N) means the server accepted the connection but went quiet. We measured what the timeout actually limits, and how retries change the exception you have to catch.
Test server
Every block uses this local server: /slow waits 3 s before answering, /drip sends one byte every 0.5 s.
# helper: tiny local server on port 8963 with a slow endpoint and a drip endpoint
import http.server, threading, time
class H(http.server.BaseHTTPRequestHandler):
def log_message(self, *a): pass
def do_GET(self):
if self.path == "/slow": # waits 3s before answering
time.sleep(3)
self.send_response(200); self.send_header("Content-Length", "10"); self.end_headers()
for ch in b"0123456789":
self.wfile.write(bytes([ch])); self.wfile.flush()
if self.path == "/drip": time.sleep(0.5) # one byte every 0.5s
do_POST = do_GET
def start():
s = http.server.ThreadingHTTPServer(("127.0.0.1", 8963), H)
s.handle_error = lambda *a: None # client hung up on /slow: ignore
threading.Thread(target=s.serve_forever, daemon=True).start()
1. The exact ReadTimeout
import requests, srv
srv.start()
try:
requests.get("http://127.0.0.1:8963/slow", timeout=1)
except requests.exceptions.ReadTimeout as e:
print("ReadTimeout:", e)
print(issubclass(requests.exceptions.ReadTimeout, requests.exceptions.Timeout),
issubclass(requests.exceptions.ReadTimeout, requests.exceptions.ConnectionError))
Output:
ReadTimeout: HTTPConnectionPool(host='127.0.0.1', port=8963): Read timed out. (read timeout=1)
True False
Measured with requests 2.32.3 / urllib3 2.3.0: the server accepted the connection but sent nothing for longer than timeout. ReadTimeout is a Timeout but not a ConnectionError, so except requests.ConnectionError does not catch it. Catch requests.Timeout (or RequestException).
2. timeout is per read, not a total limit
import requests, srv, time
srv.start()
t0 = time.monotonic()
r = requests.get("http://127.0.0.1:8963/drip", timeout=1) # each byte arrives within 1s
print(r.status_code, r.text, f"took {time.monotonic()-t0:.1f}s with timeout=1")
Output:
200 0123456789 took 4.5s with timeout=1
Measured: with timeout=1, a response that trickles in one byte every 0.5 s never times out; it took 4.5 s. The read timeout is the longest gap between bytes, not the time for the whole request. A slow-drip server can hold a request open far past your timeout. See section 5 for a total deadline.
3. Connect vs read: use a tuple
import requests, time
t0 = time.monotonic()
try: # 10.255.255.1: non-routable, the connect attempt just hangs
requests.get("http://10.255.255.1/", timeout=(0.5, 30))
except requests.exceptions.ConnectTimeout as e:
print("ConnectTimeout after", f"{time.monotonic()-t0:.1f}s")
print(issubclass(requests.exceptions.ConnectTimeout, requests.exceptions.ConnectionError))
Output:
ConnectTimeout after 0.5s
True
Measured: timeout=(0.5, 30) means 0.5 s to connect, 30 s per read, and a non-routable address failed with ConnectTimeout after 0.5 s. Unlike ReadTimeout, ConnectTimeout is a subclass of ConnectionError. Use a short connect timeout and a read timeout sized to how slow the server legitimately is.
4. With Retry, a GET ReadTimeout turns into ConnectionError
import requests, srv
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
srv.start()
s = requests.Session()
s.mount("http://", HTTPAdapter(max_retries=Retry(total=2, read=2, backoff_factor=0)))
import time
for method in ("GET", "POST"):
t0 = time.monotonic()
try:
s.request(method, "http://127.0.0.1:8963/slow", timeout=1)
except requests.exceptions.RequestException as e:
print(method, type(e).__name__, f"after {time.monotonic()-t0:.1f}s,",
"is ReadTimeout:", isinstance(e, requests.exceptions.ReadTimeout))
if method == "GET": print(" ", e)
Output:
GET ConnectionError after 3.0s, is ReadTimeout: False
HTTPConnectionPool(host='127.0.0.1', port=8963): Max retries exceeded with url: /slow (Caused by ReadTimeoutError("HTTPConnectionPool(host='127.0.0.1', port=8963): Read timed out. (read timeout=1)"))
POST ReadTimeout after 1.0s, is ReadTimeout: True
Measured: after mounting Retry(total=2, read=2), the GET retried (3 tries, 3.0 s) and then raised ConnectionError: Max retries exceeded ... (Caused by ReadTimeoutError(...)), which is not a ReadTimeout, so an except ReadTimeout handler silently stops matching once you add retries. The POST was not retried at all (Retry's default allowed_methods leaves out POST) and raised a plain ReadTimeout after 1.0 s. Catch both, or catch RequestException.
5. A real total deadline
import requests, srv, time
srv.start()
def get_with_deadline(url, deadline=2.0, timeout=1):
t0 = time.monotonic(); body = b""
with requests.get(url, timeout=timeout, stream=True) as r:
for chunk in r.iter_content(1):
body += chunk
if time.monotonic() - t0 > deadline:
raise TimeoutError(f"total deadline {deadline}s exceeded after {len(body)} bytes")
return body
try:
get_with_deadline("http://127.0.0.1:8963/drip")
except TimeoutError as e:
print("TimeoutError:", e)
Output:
TimeoutError: total deadline 2.0s exceeded after 5 bytes
With stream=True and iter_content() you can check the elapsed time yourself and give up on a slow-drip response. Measured: it stopped after 2 s with 5 of 10 bytes. More on raising timeouts: requests Max retries exceeded.
Frequently asked questions
How do I fix requests ReadTimeout?
The server connected but sent nothing within the read timeout. Raise the read part with timeout=(connect, read), add retries for idempotent requests, or fix the slow endpoint. We measured each case with requests 2.32.3.
Is requests timeout a total timeout?
No. It's the maximum gap between bytes. We measured a response that dripped one byte every 0.5 s finishing after 4.5 s with timeout=1. Use stream=True and check elapsed time for a total deadline.
Why doesn't except ReadTimeout catch my timeout?
With an HTTPAdapter Retry mounted, the final error is ConnectionError (Max retries exceeded, Caused by ReadTimeoutError). We measured that for GET. Catch requests.RequestException.
Does except ConnectionError catch ReadTimeout?
No. We measured ReadTimeout is a Timeout but not a ConnectionError; ConnectTimeout is both.
Written with AI assistance. Measured on 2026-10-09 with Python 3.13.5, requests 2.32.3 and urllib3 2.3.0 against a local test server (included above): ReadTimeout and its class hierarchy, a slow-drip response vs timeout=1, a connect/read tuple against a non-routable address, Retry with GET and POST, and a stream=True total deadline. Every block was run verbatim and its output pasted from that run. PlainHeaders is our extension; requests, urllib3 and Python are free.