PythonMastery
reference 4 min read · lesson 32 of 45 in Errors

TimeoutError / requests.exceptions.ReadTimeout

1 · The lesson

read

What 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:

python
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.

python
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. requests has 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.

python
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.

python
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:

python
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 ReadTimeout was 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.

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().