TimeoutError / requests.exceptions.ReadTimeout
1 · The lesson
readWhat this error means
You asked, and nothing came back within the time you allowed. Unlike a refusal, which is an immediate "no", a timeout means silence — the packets may have arrived, the server may still be working on it, and you have no way to know which.
That uncertainty is the whole difficulty. A refused connection tells you nothing happened. A timeout tells you nothing definite.
When you see it
Two different timeouts, and they mean different things:
requests.exceptions.ConnectTimeout: HTTPSConnectionPool(host='api.example.com', port=443): Max retries exceeded with url: /v1/charges (Caused by ConnectTimeoutError(...))
The connection was never established — usually a firewall dropping packets, or the host is down.
requests.exceptions.ReadTimeout: HTTPSConnectionPool(host='api.example.com', port=443): Read timed out. (read timeout=5)
The connection succeeded and the request was sent. The server received it and may well have acted on it. You just did not wait long enough for the answer.
Why it happens
- The server genuinely is slow — a report query, a cold start, an overloaded worker pool.
- A firewall is dropping packets silently instead of rejecting them. Dropping produces a hang; rejecting produces
ConnectionRefusedError. - You set no timeout at all, and something else in the stack gave up first.
requestshas no default timeout — without one it can wait indefinitely, and one stuck call can occupy a worker until the process is restarted. - Connection pool exhaustion: your request is queued behind others that are themselves stuck.
How to fix it
Always pass a timeout. Always.
import requests # (connect timeout, read timeout) — separate, because they mean different things r = requests.get("https://api.example.com/orders", timeout=(3.05, 10))
The odd 3.05 is deliberate: connect timeouts are best set slightly above a multiple of 3, because TCP retransmits on a 3-second cycle.
Retry a read timeout only when the operation is safe to repeat. A GET is; a POST that charges a card is not.
from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry retry = Retry( total=3, backoff_factor=0.5, # 0.5s, 1s, 2s status_forcelist=(500, 502, 503, 504), allowed_methods=("GET", "HEAD", "OPTIONS"), # deliberately not POST ) session = requests.Session() session.mount("https://", HTTPAdapter(max_retries=retry))
setup added so this can run · defines requests
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) requests = _AutoMock('requests')
To retry a write safely, make it idempotent — send a key the server can use to recognise a repeat:
session.post( "https://api.example.com/charges", json=payload, headers={"Idempotency-Key": str(order_id)}, # same key = same charge, never two timeout=(3.05, 10), )
setup added so this can run · defines session, payload, order_id
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) session = _AutoMock('session') payload = _AutoMock('payload') order_id = _AutoMock('order_id')
Set your timeout shorter than whatever is waiting on you. If your web server kills a request at 30s, an outbound call with a 60s timeout can never finish — it only holds the worker until the server gives up.
When you'd actually see this in real code
- A third-party API degrades at peak. Without timeouts every worker ends up blocked on it, and your service goes down with theirs.
- A payment charged twice, because a
ReadTimeoutwas retried without an idempotency key. The first request succeeded; only the response was lost. - A container talking to a database through a gateway that silently drops idle connections — the pool hands out a dead socket and the next query hangs.
- A health check timing out under load, so the orchestrator restarts a service that was merely busy.
Related errors
- [ConnectionRefusedError: [Errno 111] Connection refused](../error-connection-refused/) — an immediate no, rather than silence.
- socket.gaierror: Name or service not known — failed before any connection was attempted.
- SSLCertVerificationError: certificate verify failed — connected, then failed the handshake.
See Also
- All Python errors — the full index, by type and by when it happens.
- APIs & HTTP — request patterns, status codes, retries.
- Async — timeouts with
asyncio.timeout().