Guide

Python requests timeout: set it, handle it and fix proxy timeouts

How to set a Python requests timeout, which error each stall raises, and why a dead proxy slips past except Timeout. Tested on requests 2.31 to 2.34.

HProxy Team··Updated October 10, 2026·10 min read
HProxy.Guide

Skip the dead lists.

Our free proxy list re-checks every exit every few minutes across 100+ countries, with a live last-checked time, so you copy IPs that worked moments ago, not a stale text dump.

Open the free proxy list→

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.

MethodStopped afterKept the deadline
stream=True, clock check inside iter_content(chunk_size=None)20.2 sNo
stream=True, clock check inside iter_content(chunk_size=1)8.0 sYes, one byte late
future.result(timeout=7) on a thread7.0 sYes

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 stalledurllib3 2.x (requests 2.31.0 and 2.34.2)urllib3 1.26.20 (requests 2.32.3, Windows)
Server never answers, timeout=5ReadTimeout after 5 sReadTimeout after 5.0 s
Nobody at the address, (3.05, 27)ConnectTimeout after 3.1 sConnectTimeout after 3.1 s
Proxy port closedProxyError at onceProxyError after 2.1 s
Proxy at an address nobody answers, (3.05, 10)ProxyError after 3.1 sConnectTimeout after 3.1 s
Proxy accepts, never answers, http:// site, (3.05, 5)ReadTimeout after 5.0 sReadTimeout after 5.0 s
Proxy accepts, never answers, https:// site, (3.05, 5)ReadTimeout after 3.1 sProxyError after 3.1 s
Proxy answers 504, http:// siteHTTP 504 response, no errorHTTP 504 response, no error
Proxy answers 504, https:// siteProxyError at onceProxyError at once
Our terminal running the timeout test on Windows with Python 3.13.7, requests 2.32.3 and urllib3 1.26.20: one line per stall, the error class, whether except Timeout catches it, and the time it took.
Our own capture, run on 10 October 2026. Every server and proxy in the test is a stub on the same machine; 192.0.2.1 is an address from the range kept for examples.

Three results matter most when you work through proxies:

  • except requests.Timeout missed 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-After header is respected on 413, 429 and 503 replies.
  • read counts 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 the HTTP_PROXY, HTTPS_PROXY, NO_PROXY and ALL_PROXY environment 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:

  1. Run the same call without any proxy, with the proxy environment variables unset. If it works, the proxy hop is the slow part.
  2. 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.
  3. Set both timeout values and turn retries down to zero while you test, so each run shows one clean error.
  4. Print the full error. "Unable to connect to proxy", "Tunnel connection failed: 504" and "Read timed out" point to three different places.
  5. 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.

Frequently asked questions

Does Python requests have a default timeout?
No. Without a timeout argument, requests waits as long as the connection stays open. The requests documentation says nearly all production code should set one. In our test, a call to a server that never answered was still waiting after 30 seconds.
What is the difference between the connect timeout and the read timeout?
The connect timeout limits how long requests waits to open the connection. The read timeout limits the gap between two bytes from the server once the request is sent. One number sets both. A pair such as (3.05, 27) sets them apart.
Does the requests timeout limit the total download time?
No. The read timeout starts again each time a byte arrives. In our test, a reply that sent one byte every 2 seconds finished after 20 seconds with timeout=5. For a total limit, run the call in a thread and wait on it with a deadline.
Why does except requests.Timeout not catch my proxy error?
Because a proxy failure often comes as ProxyError, which is a kind of ConnectionError and not a kind of Timeout. With urllib3 2.x, a proxy that cannot be reached raised ProxyError in our test. Catch requests.exceptions.ProxyError next to requests.Timeout.
How do I retry a request that timed out?
Mount an HTTPAdapter with a urllib3 Retry object on a Session, with counts for connect and read errors, a backoff factor and the status codes to retry. A connect timeout is safe to retry. A read timeout may mean the server already got the request, so retry only calls that are safe to repeat.
Why does my proxy check take minutes even with timeout=5?
The timeout applies to each connection attempt and to each gap between bytes, not to the whole call, and retries add their own waits. In our test, three retries turned a 3.05 second connect timeout into 15.2 seconds. Keep retries low and wrap the call in a thread with a deadline when you need a hard limit.

Get proxies that are alive right now

Our free proxy list re-checks every exit every few minutes across 100+ countries, with a live last-checked time, so you copy IPs that worked moments ago, not a stale text dump. When the location has to survive a real check, the paid network holds up.

129M+ proxy checks run · 100+ countries · HTTP / HTTPS / SOCKS · re-checked every few minutes · no signup

HProxy.

Honest guides and comparisons on proxies, scraping and staying unblocked, from the team that runs the network.

RSS feed