Python requests has no default timeout, so give every call one: requests.get(url, timeout=(3.05, 27)). The first number limits how long the connection may take to open. The second limits the wait between bytes. A stall then raises requests.ConnectTimeout or requests.ReadTimeout, and except requests.Timeout catches both. Through a proxy, a dead or stalled proxy often raises requests.exceptions.ProxyError instead, which that except does not catch.
We tested every case on 10 October 2026, with requests 2.31.0, 2.32.3 and 2.34.2, the current release. The servers and proxies were small stubs we wrote, on our own machines. The error class for some proxy failures changed between urllib3 1.26 and urllib3 2.x, and the table below shows both.
What the timeout errors look like
These are the messages our test printed with requests 2.34.2 and urllib3 2.8.0. The address 192.0.2.1 comes from a range kept for examples, where nothing answers.
ReadTimeout: HTTPConnectionPool(host='127.0.0.1', port=45173): Read timed out. (read timeout=5)
ConnectTimeout: HTTPConnectionPool(host='192.0.2.1', port=80): Max retries exceeded with url: / (Caused by ConnectTimeoutError(<HTTPConnection(host='192.0.2.1', port=80) at 0x7c1086dd0410>, 'Connection to 192.0.2.1 timed out. (connect timeout=3.05)'))
ProxyError: HTTPConnectionPool(host='192.0.2.1', port=8080): Max retries exceeded with url: http://example.com/ (Caused by ProxyError('Unable to connect to proxy', ConnectTimeoutError(<HTTPConnection(host='192.0.2.1', port=8080) at 0x7c108721d190>, 'Connection to 192.0.2.1 timed out. (connect timeout=3.05)')))
"Read timed out" means the connection opened and then nothing came back in time. "Connect timeout" means the connection never opened. "Unable to connect to proxy" means the first hop failed, and the error is a ProxyError, even though a timeout sits inside it.
Does requests have a default timeout?
No. The requests documentation says it plainly: "By default, requests do not time out unless a timeout value is set explicitly. Without a timeout, your code may hang for minutes or more." Its quickstart adds that "nearly all production code should use this parameter in nearly all requests."
We sent a request to a stub server that read the request and never answered. With no timeout, the call was still waiting when our watchdog stopped it after 30 seconds. The same call with timeout=5 raised ReadTimeout after 5.0 seconds.
How do you set a timeout in requests?
Pass one number for both limits, or a pair for the connect and read limits apart.
import requests
r = requests.get("https://example.com/", timeout=10) # 10 s to connect, 10 s between bytes
r = requests.get("https://example.com/", timeout=(3.05, 27)) # 3.05 s to connect, 27 s between bytes
Three details from the requests documentation decide good values:
- The connect value works best "slightly larger than a multiple of 3", because 3 seconds is the default TCP retransmission window. That is why the documentation's own example uses 3.05.
- The connect timeout applies to each IP address tried. A host with an IPv4 and an IPv6 address that both fail can take twice as long.
- Neither value is a wall clock: a call can take longer than the numbers you set.
Is the timeout a limit on the whole request?
No. The quickstart says the timeout "is not a time limit on the entire response download". An error comes only when no bytes arrive for that many seconds. Our stub sent a 10-byte body one byte every 2 seconds. With timeout=5, the call did not fail. It returned HTTP 200 after 20.2 seconds.
When you need a hard limit, you have to build it. We tried three ways with a 7-second deadline against the same slow body.
| Method | Stopped after | Kept the deadline |
|---|---|---|
stream=True, clock check inside iter_content(chunk_size=None) | 20.2 s | No |
stream=True, clock check inside iter_content(chunk_size=1) | 8.0 s | Yes, one byte late |
future.result(timeout=7) on a thread | 7.0 s | Yes |
The first method looks right, and it failed. The loop got the whole body as one chunk, so the clock check came after 20 seconds. Small chunks fix that, at the cost of many tiny reads. A thread with a deadline is the cleanest:
from concurrent.futures import ThreadPoolExecutor
import requests
pool = ThreadPoolExecutor(max_workers=8)
def get_within(url, deadline, **kwargs):
kwargs.setdefault("timeout", (3.05, 27)) # still needed: the thread keeps running
return pool.submit(requests.get, url, **kwargs).result(timeout=deadline)
result(timeout=...) raises TimeoutError, the built-in one since Python 3.11. The Python documentation is clear that this stops the wait, not the call: a running call cannot be cancelled. Keep the normal timeout so the thread ends on its own.
Which exception does each stall raise?
We ran each case in three setups: Linux with urllib3 2.0.7 and 2.8.0, and Windows with urllib3 1.26.20. The two urllib3 2.x runs gave the same results.
| What stalled | urllib3 2.x (requests 2.31.0 and 2.34.2) | urllib3 1.26.20 (requests 2.32.3, Windows) |
|---|---|---|
Server never answers, timeout=5 | ReadTimeout after 5 s | ReadTimeout after 5.0 s |
Nobody at the address, (3.05, 27) | ConnectTimeout after 3.1 s | ConnectTimeout after 3.1 s |
| Proxy port closed | ProxyError at once | ProxyError after 2.1 s |
Proxy at an address nobody answers, (3.05, 10) | ProxyError after 3.1 s | ConnectTimeout after 3.1 s |
Proxy accepts, never answers, http:// site, (3.05, 5) | ReadTimeout after 5.0 s | ReadTimeout after 5.0 s |
Proxy accepts, never answers, https:// site, (3.05, 5) | ReadTimeout after 3.1 s | ProxyError after 3.1 s |
Proxy answers 504, http:// site | HTTP 504 response, no error | HTTP 504 response, no error |
Proxy answers 504, https:// site | ProxyError at once | ProxyError at once |

Three results matter most when you work through proxies:
except requests.Timeoutmissed the dead proxy on urllib3 2.x and the stalled tunnel on urllib3 1.26. The same failure has a different class on each.- For an
https://site, the proxy first has to answer a CONNECT request. When it stalled there, the call failed after 3.1 seconds, the connect value, not after the read value of 5. - A 504 from the proxy is a normal response for an
http://site. Your code only sees it if it checks the status.
Why does except requests.Timeout miss proxy errors?
Because of how the classes are built. In the requests source, ProxyError is defined as class ProxyError(ConnectionError), and ConnectTimeout as class ConnectTimeout(ConnectionError, Timeout). So a ConnectTimeout is both a connection error and a timeout, but a ProxyError is only a connection error. When urllib3 reports that the proxy failed, requests raises ProxyError, even if a timeout caused it.
There is a second trap. ProxyError is not part of the top-level requests package. We checked requests 2.31.0, 2.32.3 and 2.34.2: requests.ProxyError, requests.RetryError and requests.SSLError do not exist. They live in requests.exceptions. Our first draft of the code below used requests.ProxyError. It ran fine until a proxy failed, and then it crashed with AttributeError: module 'requests' has no attribute 'ProxyError'.
This version ran as shown on both urllib3 lines:
import requests
def fetch(url, proxies=None):
try:
r = requests.get(url, proxies=proxies, timeout=(3.05, 27))
r.raise_for_status()
return r
except requests.exceptions.ProxyError:
... # the proxy hop failed: try another proxy or report this one
except requests.ConnectTimeout:
... # the connection never opened: safe to retry
except requests.ReadTimeout:
... # connected, then silence: retry only calls that are safe to repeat
except requests.HTTPError:
... # a status such as 503 from the site or 504 from an http:// proxy
except requests.RequestException:
... # anything else that requests raised
The order matters. ProxyError comes first, so a proxy failure lands there on urllib3 2.x. On urllib3 1.26 the same dead proxy went to the ConnectTimeout branch, which is also a fair place for it. The documentation calls a ConnectTimeout "safe to retry", because no request reached the server.
How do you retry timeouts with backoff?
Requests does not retry on its own. Its documentation says a plain number for max_retries "applies only to failed DNS lookups, socket connections and connection timeouts, never to requests where data has made it to the server." For timeouts and status codes, pass a urllib3 Retry object:
import requests
from requests.adapters import HTTPAdapter
from urllib3.util import Retry
retry = Retry(
total=3,
connect=3,
read=1,
backoff_factor=0.5,
status_forcelist=[502, 503, 504],
)
session = requests.Session()
session.mount("http://", HTTPAdapter(max_retries=retry))
session.mount("https://", HTTPAdapter(max_retries=retry))
r = session.get("https://example.com/", timeout=(3.05, 27))
We ran this block as written. Against a server that always answered 503, it gave up after 3.0 to 3.2 seconds with RetryError, not with the last 503. Against an address where nobody answers, it raised ConnectTimeout after 15.2 seconds: four tries of 3.05 seconds, plus 0, 1 and 2 seconds of backoff. Retries make each timeout longer in total, so keep them low while you debug.
Points from the urllib3 documentation that change what gets retried:
- The wait between tries is
backoff_factor * (2 ** (number of previous retries)), capped at 120 seconds by default. - POST is not in the default list of methods to retry. It holds DELETE, GET, HEAD, OPTIONS, PUT and TRACE.
- A
Retry-Afterheader is respected on 413, 429 and 503 replies. readcounts errors "raised after the request was sent to the server", so a retried read can repeat a request the server already handled.
What timeout should you use through a proxy?
The connect value has more work to do through a proxy. For an https:// site, it has to cover the connection to the proxy and the proxy's answer to CONNECT, which waits for the proxy to reach the site. In our test, that whole step was held to the connect value. A connect value that is fine for a direct call can be too short behind a busy proxy.
Two other things often explain a "random" proxy timeout:
- A proxy you did not set. When the code passes no
proxies, requests uses theHTTP_PROXY,HTTPS_PROXY,NO_PROXYandALL_PROXYenvironment variables. A forgotten one can be the hop that stalls. - A 504 from the proxy. In HTTP, 504 means a gateway or proxy "did not receive a timely response from an upstream server". The proxy reached nothing in time, so the problem is past the proxy or at it, not in your code. Our guide to 504 gateway timeouts through a proxy covers that side.
When none of this works
Work from the outside in:
- Run the same call without any proxy, with the proxy environment variables unset. If it works, the proxy hop is the slow part.
- Test the proxy on its own with our free proxy checker. It reports whether a proxy is alive, with its protocols and latency, and needs no account.
- Set both timeout values and turn retries down to zero while you test, so each run shows one clean error.
- Print the full error. "Unable to connect to proxy", "Tunnel connection failed: 504" and "Read timed out" point to three different places.
- If the slow hop is a free or crowded proxy that stalls again and again, no timeout setting will fix it. A line with its own gateway, such as our residential proxies, answers with documented status codes instead: a 5xx is a gateway error to retry with backoff, and one that keeps coming back is ours to chase.
For the next steps around requests and proxies, see how to use proxies with Python requests and the list of every proxy error explained.
What this page could not check
Our stalls came from stub servers and stub proxies on our own machines, not from a real slow site or a real proxy network. Real calls add DNS and network delay on top of the numbers here. We did not test SOCKS proxies, HTTPX, aiohttp or async code. We saw the urllib3 1.26 and 2.x difference on one version of each line, and on two operating systems. Windows took 2.1 seconds to report a closed local port where Linux took none, and we did not trace why. requests and urllib3 change how they map errors from release to release, so we will run the test again by 15 January 2027.
Sources
- Requests documentation, Advanced Usage: Timeouts and Proxies, for Requests 2.34.2, read 10 October 2026.
- Requests documentation, Quickstart: Timeouts, and Errors and Exceptions, read 10 October 2026.
- Requests API reference: exceptions and HTTPAdapter max_retries, read 10 October 2026.
- Requests source code, src/requests/exceptions.py and adapters.py, psf/requests on GitHub, read 10 October 2026.
- urllib3 documentation, urllib3.util.Timeout and urllib3.util.Retry, read 10 October 2026.
- Python documentation, concurrent.futures: Future.result and Future.cancel, read 10 October 2026.
- RFC 9110, HTTP Semantics, IETF, June 2022: 408 Request Timeout and 504 Gateway Timeout.
- PyPI, requests release history: version 2.34.2, released 14 May 2026.
- HProxy documentation: gateway status codes and the free proxy checker API, read 10 October 2026.
- Our own tests on 10 October 2026: timeouts.py, deadline.py and final_code.py, run with requests 2.31.0, 2.32.3 and 2.34.2.


