How to Set an HTTP Client Timeout to Stop Cascading Failures
No timeout on an outbound HTTP client means one hung downstream can exhaust your worker pool and bring down your entire API. Adding an explicit httpx.Timeout makes the call fail fast and return a 503 - not hang indefinitely.
Why no timeout causes cascading failure
When your API calls a downstream service and that service stalls - slow response, dropped connection, overloaded server - your request handler blocks waiting for a reply that never comes. With ~20 concurrent requests all waiting, every worker thread is occupied. The load balancer still marks you healthy (you're accepting connections), so traffic keeps arriving, and the queue grows until the whole service is dead.
The root cause is the default httpx.Client() with no timeout - which means
infinite wait. Reproducing it is trivial:
curl --max-time 8 http://localhost:8000/shipping/123
# hangs for 8 seconds - blocked by the stalled downstream
Set an explicit httpx.Timeout
httpx.Timeout lets you specify timeouts for each phase of the request lifecycle
separately. The first positional argument is the default applied to all four phases
(connect, read, write, pool); per-phase overrides are keyword arguments:
import httpx
from fastapi import FastAPI
from fastapi.responses import JSONResponse
app = FastAPI()
client = httpx.Client(
base_url="http://downstream-service",
timeout=httpx.Timeout(3.0, connect=2.0),
)
@app.get("/shipping/{order_id}")
def get_shipping(order_id: str):
try:
resp = client.get(f"/orders/{order_id}")
resp.raise_for_status()
return resp.json()
except httpx.TimeoutException:
return JSONResponse({"error": "upstream timeout"}, status_code=503)
except httpx.HTTPError as exc:
return JSONResponse({"error": str(exc)}, status_code=502)
Here connect=2.0 caps the TCP handshake and 3.0 caps the read phase. The
downstream now has at most 3 seconds to respond - then the request fails fast
and the worker is freed immediately.
Tune each timeout phase independently
| Phase | What it covers | Typical value |
|---|---|---|
connect |
TCP handshake + TLS | 1-3 s |
read |
Time between bytes arriving | 5-30 s (depends on the SLO) |
write |
Sending the request body | 5 s |
pool |
Waiting for a free connection in the pool | 5 s |
Set read based on what the downstream's SLO actually promises - not an arbitrary
large value. An SLO of "p99 response in 2s" means your read timeout should be
around 3-4s, not 30.
Catch the right exception
httpx raises httpx.TimeoutException (a subclass of httpx.HTTPError) on any
timeout. Always catch it explicitly and return a 503 Service Unavailable - that
signals "I am healthy but a dependency is not," which lets upstream retry logic and
circuit breakers work correctly.
For the next layer of resilience, combine this with a circuit breaker (open after N consecutive timeouts, stop calling the downstream entirely for a backoff period).
Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.
Try it in a workspace →What you'll practice
- Setting httpx.Timeout with per-phase overrides (connect, read)
- Catching httpx.TimeoutException and returning a 503 response
- Tuning timeout values to match the downstream service's SLO
FAQ
What happens if I don't set an HTTP client timeout?
Your request handler blocks indefinitely waiting for the downstream reply. Under load, all worker threads fill up with blocked requests, the service stops responding to new traffic, and you get a cascading failure - even though the downstream issue is temporary.
How do I set a timeout with httpx in Python?
Pass a timeout to the client: httpx.Client(timeout=httpx.Timeout(3.0, connect=2.0)). The first positional arg is the default for all phases; keyword args override individual phases (connect, read, write, pool). Catch httpx.TimeoutException and return 503.
What HTTP status code should I return on a client timeout?
Return 503 Service Unavailable - it means your service is healthy but a dependency is not. This lets upstream load balancers and retry logic distinguish a downstream failure from a bug in your own code.
What is a good HTTP client timeout?
Set both a connect timeout (1-3s) and a read timeout sized to the slowest acceptable response (often 5-30s). The key rule is to always set one - many clients default to no timeout, which lets a single slow call hang a worker forever.
Keep learning
Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.
Open the workspace →