Python Requests Timeout Guide: GET, POST, and Session Defaults
Jul 21, 2026 · Guides · 10 min read
Python Requests does not set a timeout automatically. This guide shows safe GET and POST examples, explains connect and read timeout behavior, handles timeout exceptions, sets a real Session default, adds restrained retries, and diagnoses direct-versus-proxy failures.
A Python Requests timeout is not enabled automatically. If you call requests.get() or requests.post() without a timeout argument, Requests can keep waiting while the network stack, remote server, or an intermediary remains unresponsive. The practical fix is to pass an explicit timeout to every external request and handle the resulting exception deliberately.
The important detail is what that value means. A Requests timeout is not a stopwatch for the complete request and response download. It controls how long Requests waits during connection establishment and how long it tolerates a pause while reading response data. You can use one value for both phases or a tuple that gives each phase a separate limit.
Where this guide mentions local testing, it refers to the environment listed below. The guide covers normal GET and POST calls, separate connect and read values, exception handling, Session defaults, bounded retries, and proxy-specific diagnosis.
TL;DR
- Requests does not apply a timeout unless you pass one explicitly.
- Use
timeout=(connect, read)when connection setup and response delivery need different limits.- Catch timeout exceptions separately when the recovery path depends on whether the failure happened during connect or read.
- Do not treat
timeout=5as a hard wall-clock deadline for the full request lifecycle.
Quick start: a safe default pattern
If you need one copy-paste example first, use this pattern:
import requests
try:
response = requests.get(
"https://postman-echo.com/get",
timeout=(3.05, 20),
)
response.raise_for_status()
print(response.json()["url"])
except requests.exceptions.ConnectTimeout:
print("The connection took too long to establish.")
except requests.exceptions.ReadTimeout:
print("The server stopped sending data before the read timeout.")
except requests.exceptions.RequestException as error:
print(f"Another Requests error occurred: {error}")
Use a tuple when you want a shorter connect limit and a longer read limit. If you need a hard total deadline, enforce it outside Requests with a worker timeout, task budget, or other higher-level control.
Tested environment and prerequisites
| Component | Tested value | Purpose |
|---|---|---|
| Operating system | Windows 11, build 26200 | Test host |
| Python | 3.12.13 | Runtime |
| Requests | 2.34.2 | HTTP client |
| urllib3 | 2.7.0 | Transport and retry support used by Requests |
| Public test service | Postman Echo | Safe GET and POST request reflection |
Requests 2.34.2 requires Python 3.10 or newer. Install the tested release in a virtual environment so the tutorial does not change packages used by another project:
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install "requests==2.34.2"
python -c "import requests; print(requests.__version__)"
On macOS or Linux, activate the environment with source .venv/bin/activate. The final command should print 2.34.2 for this exact test setup. A later Requests release may also work, but verify its documentation and rerun the examples before publishing derived instructions.
Local verification notes for this article:
- Public success examples use Postman Echo for simple GET and POST reflection.
- Timeout exception behavior was reproduced in local testing with a local HTTP server that paused before sending response bytes.
session.timeout = ...was checked in the test environment described above to confirm that the attribute exists on the object but is not consumed bySession.request().- Retry behavior was checked in local testing with a local endpoint that returned
503once and200on the next attempt.
What is the Python Requests default timeout?
The Python Requests default timeout is effectively no timeout. More precisely, the default argument is None, so Requests does not impose a connect or read limit unless your code supplies one. The official Quickstart warns that omitting the value can leave a program waiting indefinitely.
This call has no Requests-level timeout:
import requests
response = requests.get("https://postman-echo.com/get")
Adding a scalar applies the same numeric value to the connect and read phases:
import requests
response = requests.get("https://postman-echo.com/get", timeout=10)
That does not promise completion within ten wall-clock seconds. A connection can involve more than one IP-address attempt, and the read limit concerns periods without incoming bytes rather than the total duration of a large response body. Treat the value as an inactivity boundary, not as an end-to-end service-level deadline.
Set a Python Requests GET timeout
A Python Requests GET timeout is passed directly to requests.get(). Use raise_for_status() separately because a timeout and an HTTP error answer different questions: a timeout means a network phase waited too long, while 404, 429, or 500 is a completed HTTP response with an error status.
import requests
response = requests.get(
"https://postman-echo.com/get",
timeout=20,
)
response.raise_for_status()
print(response.status_code)
print(response.json()["url"])
The tested output was:
200
https://postman-echo.com/get
Do not copy 20 into every project without thought. Start from the expected latency of the service, measure normal and high-percentile response times, and leave enough margin for routine network variation. A user-facing API call and a scheduled export may need very different read limits.
Set a Python Requests POST timeout
A Python Requests POST timeout uses the same parameter. The method does not require a special timeout API; requests.post() forwards supported request options through the normal Requests pipeline.
import requests
response = requests.post(
"https://postman-echo.com/post",
json={"task": "timeout-test"},
timeout=(3.05, 20),
)
response.raise_for_status()
print(response.status_code)
print(response.json()["json"])
The tested output was:
200
{'task': 'timeout-test'}
The timeout mechanics are the same for GET and POST, but retry decisions are not. GET is normally intended to be idempotent. A POST may create an order, charge a card, or submit a job before the client loses the response. Retrying that POST automatically can duplicate the side effect unless the API supports an idempotency key or documents another safe retry mechanism.
Split connect and read timeouts
Use a two-item tuple when connection establishment and response generation have different budgets:
response = requests.get(
"https://postman-echo.com/get",
timeout=(3.05, 20),
)
The first number is the connect timeout. It limits each attempt to establish a socket connection to a resolved IP address. If a hostname resolves to multiple addresses, the underlying transport may try them sequentially, so the elapsed connection time can exceed one connect value.
The second number is the read timeout. It limits how long Requests waits between bytes after the connection is established and the request has been sent. It commonly acts as the wait for the first response byte, but it is not a cap on the total download. A server that keeps sending small chunks before the interval expires can take longer than the configured read value to finish the body.
| Form | Meaning | Appropriate use |
|---|---|---|
timeout=10 |
Apply 10 seconds to both connect and read inactivity | Simple calls with similar phase budgets |
timeout=(3.05, 20) |
Allow 3.05 seconds per connect attempt and 20 seconds between received bytes | APIs with quick connections but slower processing |
timeout=None |
Apply no Requests timeout | Rare, deliberate cases only; unsafe as a casual default |
If the application needs a hard total deadline, Requests’ normal tuple does not provide that guarantee. Enforce the deadline at a higher level, such as a worker timeout, process boundary, task queue, or client architecture designed around a total budget. Test cancellation behavior carefully because stopping a waiting thread is not the same as canceling an asynchronous operation.
Catch timeout exceptions without hiding other failures
Requests exposes separate connection and read exceptions plus their shared parent. Catch the specific types when the recovery action or log message differs:
import requests
try:
response = requests.get(
"https://postman-echo.com/get",
timeout=(3.05, 20),
)
response.raise_for_status()
except requests.exceptions.ConnectTimeout:
print("Connection setup exceeded the connect timeout")
except requests.exceptions.ReadTimeout:
print("The server stopped sending data within the read timeout")
except requests.exceptions.RequestException as error:
print(f"The request failed for another reason: {error}")
else:
print(response.status_code)
If both timeout phases have the same recovery path, catch requests.exceptions.Timeout instead. It is the parent of ConnectTimeout and ReadTimeout. Keep a broader RequestException handler after the timeout handler so DNS failures, refused connections, redirect problems, TLS errors, and HTTP errors are not mislabeled as timeouts.
In local testing with the environment described above, the exception branches were checked against a local HTTP server that paused for 0.25 seconds while the client used a 0.05-second read timeout. That run raised ReadTimeout. Depending on a public /delay endpoint for this check is fragile, which is why the successful public examples use Postman Echo instead.
Set a real default timeout for a Session
One common tutorial error is assigning a new attribute:
session = requests.Session()
session.timeout = 10 # This does not configure Session.request().
Python allows that attribute to exist, but Requests does not read it when building the request. In local testing with the environment described above, session.timeout = 0.05 was followed by a local response that took more than 0.20 seconds, and the request still completed.
Create a small Session subclass and supply the default through the actual request keyword arguments:
import requests
class TimeoutSession(requests.Session):
def __init__(self, timeout=(3.05, 20)):
super().__init__()
self.default_timeout = timeout
def request(self, method, url, **kwargs):
kwargs.setdefault("timeout", self.default_timeout)
return super().request(method, url, **kwargs)
with TimeoutSession() as session:
response = session.get("https://postman-echo.com/get")
response.raise_for_status()
print(response.status_code)
setdefault() preserves an explicit per-call override:
with TimeoutSession() as session:
response = session.get("https://postman-echo.com/get", timeout=(5, 60))
response.raise_for_status()
A caller can also pass timeout=None, which intentionally disables the default because the key is present. Decide whether your wrapper should permit that behavior. For a strict internal client, you may reject None instead of silently accepting it.
Add retries only where the operation is safe
Requests does not retry failed connections by default. You can mount an HTTPAdapter with urllib3’s Retry policy, but keep attempts bounded and restrict the allowed methods:
import requests
from requests.adapters import HTTPAdapter
from urllib3.util import Retry
retry_policy = Retry(
total=2,
status_forcelist=[429, 502, 503, 504],
allowed_methods={"GET", "HEAD"},
backoff_factor=0.5,
respect_retry_after_header=True,
)
with requests.Session() as session:
session.mount("https://", HTTPAdapter(max_retries=retry_policy))
response = session.get(
"https://postman-echo.com/get",
timeout=(3.05, 20),
)
response.raise_for_status()
In local testing with the environment described above, the retry mechanism was checked with a server that returned 503 once and 200 on the next request. The final status was 200, and the server recorded exactly two attempts.
Retries are not a substitute for choosing a sensible timeout. Each new attempt consumes time and server capacity. Respect Retry-After, avoid unbounded loops, add jitter in large distributed workloads, and do not add POST to allowed_methods unless the operation is demonstrably idempotent.
Choose timeout values from evidence
There is no universal best Python HTTP request timeout. The same is true for any broader Python request timeout policy: a useful value comes from the operation’s latency distribution, user-facing deadline, body size, network path, and failure cost.
Use this process:
- Measure connection and response timing under normal load.
- Separate fast connection setup from slow server processing with a tuple.
- Test high-latency regions and any proxy route the production job will use.
- Decide how many attempts fit inside the operation’s total time budget.
- Log the timeout phase, URL host, attempt number, elapsed time, and correlation ID without logging credentials or sensitive response data.
- Review the values when the endpoint, region, payload, proxy, or service-level objective changes.
Short values fail fast but can reject healthy requests during routine variance. Long values reduce false timeouts but tie up workers and delay failure handling. The right tradeoff is an operational decision, not a magic number copied from a tutorial.
Troubleshoot Python HTTP request timeout failures
| Symptom | Likely cause | How to verify | Practical action |
|---|---|---|---|
| The script appears to hang | No timeout was supplied | Log timestamps immediately before and after the call | Add an explicit scalar or tuple and catch the timeout |
ConnectTimeout occurs |
Route, firewall, endpoint, or connection path is slow or unavailable | Test DNS resolution, TCP reachability, another network, and repeated attempts | Fix reachability; adjust connect value only after measurement |
ReadTimeout occurs after connecting |
Server processing or response delivery paused | Compare server logs, time to first byte, payload size, and a direct request | Increase read budget when justified or reduce server work/body size |
| Elapsed time exceeds the tuple values | Multiple IP attempts or continuing response bytes | Record resolved addresses and stream timing | Do not treat the tuple as a wall-clock deadline |
| Only proxied calls time out | Proxy route, region, authentication, or endpoint health differs | Send the same authorized request directly and through the proxy; compare phase and latency | Fix credentials or route, choose a healthier endpoint, then tune values |
Timeout appears with 403 or 429 |
The timeout and access/rate-limit response may be separate problems | Log status codes and relevant response headers before retrying | Reduce request rate, respect policy, and follow documented access rules |
| Retries make the job slower | Attempts are too numerous or backoff exceeds the budget | Log attempt count and cumulative elapsed time | Lower retry count, cap backoff, and fail clearly |
Avoid disabling TLS verification to make a timeout disappear. verify=False changes certificate validation and exposes the connection to man-in-the-middle risk; it does not solve connect/read budgeting.
Diagnose timeouts in proxy and scraping workflows
In legitimate public web data collection, a timeout may occur at the target server, local network, proxy connection, or response path. Change one variable at a time. First run the same authorized request directly. Then run it through the proxy with the same URL, headers, payload, and timeout tuple. Record whether the failure is connect or read related, plus elapsed time and HTTP status when a response exists.
If the direct call succeeds consistently but the proxied call fails, inspect proxy authentication, endpoint location, route latency, session settings, and IP health before simply increasing the timeout. The Rola IP website provides product context, and the Rola IP documentation center is the better next step when you need Python proxy configuration details.
Proxy infrastructure does not replace timeout handling, bounded retries, rate limits, or application-level error reporting. It also does not grant permission to collect data. Follow the target site’s terms, applicable robots policies, contractual restrictions, rate limits, and relevant laws.
Limits that timeout does not enforce
Requests’ timeout argument does not automatically provide all of these controls:
- a guaranteed deadline for DNS resolution, connection attempts, redirects, and the full body combined;
- a maximum response-body size;
- a maximum number of redirects unless separately configured;
- automatic cancellation of unrelated application work;
- safe retries for non-idempotent operations;
- protection from a server that sends bytes slowly enough to keep resetting the read interval.
For large downloads, use streaming, validate Content-Length when available, count received bytes, and stop when your own size or elapsed-time policy is exceeded. For strict deadlines or high concurrency, evaluate a client and architecture with explicit total-time and cancellation semantics rather than forcing Requests to behave like an asynchronous runtime.
Conclusion
Reliable Requests code sets a timeout explicitly, treats connect and read phases separately when useful, and never confuses inactivity limits with a complete wall-clock deadline. Add specific exception handling, enforce Session defaults through the request arguments, and keep retries bounded to safe methods.
When a timeout appears only through a proxy, compare an equivalent direct request before changing values. That simple control test separates application behavior from route-specific latency and leads to a fix based on evidence rather than guesswork.