Python

requests ReadTimeout versus ConnectTimeout

Written and reviewed by Sahil Srivastav

PythonrequestsHTTP
requests.exceptions.ReadTimeout: HTTPSConnectionPool(host='api.example.test', port=443): Read timed out. (read timeout=5)

What this error actually means

Requests has no default timeout. Without one, a stalled connect or response can occupy a thread indefinitely, exhaust a worker pool, and turn one remote failure into a local outage.

A `ConnectTimeout` occurs before an established connection can be made: DNS, TCP, or TLS. A `ReadTimeout` means the connection exists but no response bytes arrived within the read window. The latter can occur after the server has accepted and acted on a POST, so blindly retrying it can duplicate work.

Timeouts are per network phase and usually apply to each read gap, not the complete response duration. A streaming endpoint can therefore run for hours while sending one byte every few seconds unless you impose an overall deadline.

Causes, most common first

  1. 1No timeout at all. The calling thread waits forever and consumes concurrency.
  2. 2Upstream handler or proxy stalls. The socket is open but response bytes stop arriving.
  3. 3Network path or DNS failure. Connection establishment cannot complete inside the connect budget.
  4. 4Pool starvation. A session waits for a pooled socket while callers interpret delay as remote slowness.

When you see it

  • Requests hang until worker shutdown when no timeout is supplied
  • ConnectTimeout clusters with DNS or route changes
  • ReadTimeout clusters with slow upstream handlers or stalled proxies
  • Retries increase upstream load during an incident
  • POST outcomes are unknown after a read timeout

How to diagnose it

Step 1

Classify the exception

Inspect the exception chain and elapsed time; the phase names the failing part of the path.

python - <<'PY'
import requests
try: requests.get(url, timeout=(2, 5))
except requests.exceptions.ConnectTimeout: print('connect phase')
except requests.exceptions.ReadTimeout: print('read phase')
PY

Step 2

Measure DNS, connect, and first byte

Compare curl timings to isolate the path before changing application retries.

curl -sS -o /dev/null -w 'dns=%{time_namelookup} connect=%{time_connect} ttfb=%{time_starttransfer} total=%{time_total}\n' https://api.example.test

Step 3

Check pool wait separately

Enable urllib3 connection-pool logging and inspect active worker count; a pool wait is not a server read timeout.

The fix

Pass explicit `(connect, read)` timeouts on every call, ideally through one session wrapper.

Set an overall deadline around the operation when streaming or when business latency has a hard limit.

Retry only idempotent operations and only transient phases, with exponential backoff and jitter.

Use an idempotency key for writes whose result is unknown, and reconcile before retrying.

Bound the HTTP connection pool and expose wait time so remote latency cannot consume all workers.

timeout = (2.0, 8.0)
response = session.get(url, timeout=timeout)
response.raise_for_status()

How to stop it coming back

  • Lint calls that omit timeout
  • Track connect, TTFB, read, and total latency separately
  • Budget outbound timeouts below inbound request deadlines
  • Use one retry policy per dependency
  • Exercise slow headers, slow bodies, and dropped connections in tests

Practise production debugging in a real repository

Reading about a failure and reproducing one are different skills. Gronex ships broken backend repositories with failing test suites that encode the real invariant, so you debug from evidence instead of memorising symptoms.

FAQ

What timeout should I use?

Choose it from the dependency SLA and your own deadline. A universal number is unsafe; connect is usually short, while read must cover the expected response tail.

Does read timeout mean the server failed?

No. It means no bytes arrived during the read window. The server may still be processing or may have completed a write before the client timed out.

Can I retry every ReadTimeout?

Only when the operation is idempotent or protected by an idempotency key and the retry budget is bounded.

Related

Other errors engineers hit next to this one

Full error and symptom index →