Use explicit httpx.Timeout settings on every client; the default is safer than Requests, but it is rarely the right policy for production. HTTPX gives you separate controls for connect, read, write, and pool waits, which makes failures easier to bound and debug. Requests is simpler, yet its timeout behavior can surprise teams because no timeout is applied unless you set one.
TLDR: HTTPX applies a default timeout of about 5 seconds of network inactivity, while Requests can wait indefinitely if you forget the timeout argument. For example, a worker making 20,000 API calls per hour can pile up hundreds of stuck sockets during a partner outage if Requests has no timeout. Use httpx.Client(timeout=httpx.Timeout(10.0, connect=2.0)) for predictable limits. Choose httpx.AsyncClient or aiohttp when you need high concurrency without blocking threads.
Why HTTP timeouts matter
A timeout is not just a performance setting. It is a failure policy. It decides how long your application keeps waiting before it gives up, retries, returns an error, or frees a worker.
Without strict timeouts, slow remote services can quietly exhaust your app. Threads fill up. Async tasks sit idle. Connection pools clog. The user sees a spinner, then a generic failure. Honestly, it feels like one of those bugs that should be obvious, but it still appears in production far too often.
HTTPX timeout model
HTTPX has a more detailed timeout model than Requests. It supports four timeout categories:
- Connect timeout: time allowed to establish the TCP connection, plus TLS setup when used.
- Read timeout: time allowed while waiting for data from the server.
- Write timeout: time allowed while sending request data.
- Pool timeout: time allowed while waiting for a free connection from the connection pool.
The default HTTPX behavior is cautious. It raises a timeout if network activity stalls for about 5 seconds. That is a much safer default than waiting forever, but it may be too strict for file uploads, slow reports, large downloads, or APIs with cold starts.
import httpx
timeout = httpx.Timeout(
timeout=10.0,
connect=2.0,
read=8.0,
write=8.0,
pool=1.0,
)
with httpx.Client(timeout=timeout) as client:
response = client.get("https://api.example.com/users")
response.raise_for_status()
This example gives the connection phase only two seconds. That is intentional. If a host cannot accept a connection quickly, waiting longer often wastes capacity. The read timeout is longer because the server may need time to process the request.
HTTPX async timeouts
HTTPX uses the same timeout ideas in synchronous and asynchronous clients. That consistency is one of its strongest features. You can move from httpx.Client to httpx.AsyncClient without learning a second timeout system.
import httpx
import asyncio
async def fetch_user(user_id: int):
timeout = httpx.Timeout(10.0, connect=2.0, pool=1.0)
async with httpx.AsyncClient(timeout=timeout) as client:
response = await client.get(f"https://api.example.com/users/{user_id}")
response.raise_for_status()
return response.json()
asyncio.run(fetch_user(42))
Async does not remove the need for timeouts. It only changes the cost of waiting. A blocked thread is expensive. A waiting coroutine is cheaper, but thousands of waiting coroutines still consume memory, sockets, pool slots, and attention during incidents.
Requests timeout behavior
Requests is still widely used. It is stable, familiar, and easy to read. Its timeout handling is also less complete.
By default, Requests does not set a timeout. If you call requests.get() without timeout, your code may wait without a clear upper bound. It drives me crazy that Requests still waits forever unless the developer remembers one extra argument.
import requests
response = requests.get(
"https://api.example.com/users",
timeout=(2.0, 8.0), # connect timeout, read timeout
)
response.raise_for_status()
The tuple form means connect timeout and read timeout. Requests does not expose HTTPX-style write and pool timeout controls in the same direct way. For many small scripts, that is acceptable. For services with high traffic, it can be too blunt.
HTTPX vs Requests: practical differences
| Feature | HTTPX | Requests |
|---|---|---|
| Default timeout | Yes, around 5 seconds of inactivity | No default timeout |
| Async support | Built in with AsyncClient |
Not built in |
| Timeout categories | Connect, read, write, pool | Mainly connect and read |
| HTTP/2 support | Available when enabled | Not native |
| Best fit | Services, async apps, stricter control | Scripts, simple sync clients, legacy code |
Recommended timeout values
There is no universal value, but you can start with a sane policy. Tune it with real latency data from your own systems.
- Internal APIs: connect
0.5sto2s, read2sto5s. - External APIs: connect
2s, read5sto15s. - Payment or identity calls: keep limits strict, but match provider guidance.
- Uploads: increase write timeout based on file size and client bandwidth.
- Streaming: avoid tiny read timeouts unless each chunk must arrive quickly.
A good rule is to set the connect timeout low and the read timeout based on the operation. Slow connection setup often points to DNS, routing, firewall, TLS, or service reachability problems. A longer wait rarely fixes that.
Async HTTP client alternatives
HTTPX AsyncClient is the easiest choice if you want a Requests-like API with async support. It works well with FastAPI, Starlette, AnyIO, and modern Python services. It also supports both sync and async patterns, which helps teams standardize.
aiohttp is a mature async HTTP client and server framework. It has excellent performance and is common in high-concurrency systems. Its API is not as close to Requests, so new users may need more time to get comfortable.
curl_cffi can be useful when you need behavior closer to browser-like TLS fingerprints or libcurl features. It is not the default choice for ordinary backend calls, but it solves some special scraping and compatibility cases.
urllib3 powers much of the Python HTTP ecosystem. It is reliable for sync use and provides strong connection pooling. It is not normally picked as the main async client for new asyncio services.
Common timeout mistakes
- Creating a new client for every request: this kills connection reuse and can add needless latency.
- Setting only a read timeout: connection attempts can still hurt you.
- Using huge timeouts to hide server slowness: this moves the failure from the API to your app.
- No pool timeout: under load, tasks may wait too long for a free connection.
- No logging: a timeout without context is hard to diagnose.
Expect to waste time on vague production errors if you do not log the timeout type. HTTPX helps here because it raises specific exceptions such as ConnectTimeout, ReadTimeout, WriteTimeout, and PoolTimeout.
Best practice for production
Create one shared client per service area, set explicit timeouts, set connection limits, and close the client cleanly. Pair timeouts with retries only for safe operations. Do not blindly retry payments, order creation, or any non-idempotent request unless the API gives you a safe idempotency key.
limits = httpx.Limits(
max_connections=100,
max_keepalive_connections=20,
)
timeout = httpx.Timeout(10.0, connect=2.0, pool=1.0)
client = httpx.Client(timeout=timeout, limits=limits)
If you use FastAPI or another async framework, create the async client during application startup and close it during shutdown. That avoids socket leaks and keeps connection pooling effective.
Bottom line
HTTPX is usually the better choice for modern Python services that need clear timeout behavior, async support, and fine-grained control. Requests remains fine for simple synchronous work, but only if you always pass a timeout. For high-concurrency async workloads, start with httpx.AsyncClient; choose aiohttp when you need its mature async ecosystem or very tight control over client behavior.