Python timeouts: HTTP, databases, and async deadlines
Set HTTP phase limits, database connection and statement timeouts, and cooperative async deadlines, then budget retries and handle uncertain write outcomes.
Timeouts bound how long an application is prepared to wait for an operation. They help prevent an unavailable dependency from occupying every worker or connection. They do not necessarily stop the underlying work: a thread can keep running, and a remote write may succeed after the caller stops waiting.
Configure limits at the boundaries that can block, then define cancellation, cleanup, and recovery behavior. A single HTTP read timeout does not cover every stage of an end-to-end request.
Why a deadline needs an operational response
When downstream calls remain pending, occupied workers and connections can exhaust local capacity. A timeout gives the caller a failure path, but that path still needs to release resources and avoid amplifying load through retries. Record enough context to distinguish connection trouble, pool contention, slow execution, and a cancelled caller.
Set HTTP phase timeouts
HTTPX separates connection, read, write, and connection-pool waits:
import httpx
# Five seconds to connect; ten seconds for each other timeout category.
timeout = httpx.Timeout(10.0, connect=5.0)
response = httpx.get("https://api.example.com/data", timeout=timeout)
response.raise_for_status()
Replace the illustrative URL with the intended API. A read timeout limits waiting for data, rather than setting a ten-second cap on the entire response. A response that keeps delivering data can run longer. The pool timeout matters when a client needs a pooled connection; the phase limits are documented in HTTPX timeouts.
Requests also accepts separate connection and read values:
import requests
response = requests.get(
"https://api.example.com/data",
timeout=(3.05, 27),
)
response.raise_for_status()
Requests does not set a timeout unless one is supplied. Its tuple does not provide HTTPX's separate write and pool controls, and it is not a total elapsed-time limit. DNS resolution, multiple addresses, redirects, and library-specific behavior also matter when establishing a real deadline. See Requests timeouts.
Handle httpx.TimeoutException or requests.exceptions.Timeout at a boundary that can choose a useful response. Do not catch all exceptions and classify them as timeouts: an invalid URL, certificate error, or application defect requires a different diagnosis.
Separate database connection, query, and pool limits
With PostgreSQL and psycopg 3, the connection and server-side statement limits are different settings:
import psycopg
# dsn is a connection string from controlled application configuration.
with psycopg.connect(dsn, connect_timeout=5) as connection:
with connection.cursor() as cursor:
cursor.execute("SET LOCAL statement_timeout = '10s'")
cursor.execute("SELECT 1")
connect_timeout limits connection establishment according to libpq's semantics, including separate attempts when multiple hosts are specified. statement_timeout is enforced by PostgreSQL for statements. SET LOCAL scopes the setting to the current transaction; this example uses psycopg's default transaction behavior, not autocommit.
A cancelled statement can leave the transaction failed. Let the exception exit the connection context so it rolls back, or explicitly roll back before continuing. A connection context closes the connection on exit. See psycopg transaction management and PostgreSQL statement_timeout.
For SQLAlchemy with the PostgreSQL psycopg dialect and its usual QueuePool:
from sqlalchemy import create_engine
# url uses the postgresql+psycopg dialect and controlled credentials.
engine = create_engine(
url,
connect_args={"connect_timeout": 5},
pool_timeout=10,
)
pool_timeout limits how long checkout waits for an available pooled connection. It does not set a query timeout and is not an option supported by every pool implementation. The connect_args values are passed to the driver. Configure the statement limit separately. See SQLAlchemy connection pools.
Bound async work cooperatively
Python 3.11 and later provide asyncio.timeout():
import asyncio
async def slow_call():
await asyncio.sleep(10)
return "Fresh result"
async def with_timeout():
try:
async with asyncio.timeout(0.05):
return await slow_call()
except TimeoutError:
return "Fallback result"
print(asyncio.run(with_timeout()))
Catch TimeoutError outside the timeout context: it converts the cancellation it initiates when leaving that scope. The short deadline makes this demonstration quick.
await asyncio.wait_for(slow_call(), timeout=5) is another option. It requests cancellation of the awaited operation on timeout and waits for that cancellation to finish, so elapsed time can exceed five seconds. A blocked event loop or swallowed cancellation can defeat the intended response time. Work offloaded to a thread is not forcibly terminated when an async waiter is cancelled. See asyncio timeouts.
Nested scopes follow their deadlines: the outer one can expire first. AnyIO offers move_on_after() and fail_after() with related cancellation semantics, but those use synchronous with inside async functions. See AnyIO cancellation.
Allocate a deadline budget
Start with measured latency distributions, including p95/p99, and decide how many legitimate slow operations the service can afford to time out. Include the caller's latency objective, not just the dependency's observed latency.
Within a call path, leave room for each stage and cleanup. A downstream budget of three seconds inside a five-second gateway budget and a ten-second client budget illustrates the ordering, not a universal recommendation. Parallel work, sequential dependencies, retries, and time already spent all consume that budget differently. Where possible, pass the remaining deadline rather than restarting a full timeout at every hop.
Give batch jobs and reports their own policies when their expected duration differs from interactive requests. Long-running operations may need progress or idle checks in addition to an overall business deadline.
Decide what happens after a timeout
Retry only when the operation's semantics and remaining budget permit it. Bound the attempt count and use backoff with jitter to avoid synchronized retry spikes. Three retries after an initial five-second attempt can mean four attempts, plus backoff and cleanup; a per-attempt limit is not the same as a total budget.
A fallback may return appropriately labeled cached data or another explicitly acceptable result. Circuit breaking can pause calls to a struggling dependency. Neither should hide an unacceptable failure from the caller or operators.
For writes, a timeout often means the outcome is unknown. Reuse a valid idempotency key under the receiving service's documented contract, and check operation status or reconcile before repeating a side effect. A client-generated key alone does nothing unless the server implements deduplication correctly.
Log the dependency, operation, timeout category, elapsed time, attempt count, and request identifier. When collecting these through Guance DataKit logging, compare rates by dependency and release and correlate them with downstream evidence. Timeout counts alone do not prove the remote service failed.
Common questions
Why can a service still overload after adding timeouts?
Check every blocking phase, cancellation and cleanup behavior, pool capacity, and retry amplification. A timed-out waiter may leave real work running. Bound concurrency as well as time.
Does zero mean no timeout?
The meaning is API-specific. Zero may mean an immediate timeout, while another API may use None to disable it. For SSE or WebSockets, combine heartbeats, idle limits, and an appropriate business lifetime instead of blindly reusing a short request timeout. A local Unix socket can still block and needs a considered policy.
What about a partially completed write?
Treat its outcome as uncertain until the operation can be checked. Use transactional boundaries, server-supported idempotency, and reconciliation as the system requires. Retrying the same request without that contract can duplicate the effect.
The async example and HTTP read-timeout behavior were checked locally. PostgreSQL/psycopg and production retry, pool, and Guance ingestion behavior were reviewed from documentation rather than tested on a live database or service. See also Python subprocess and Python type hints (Chinese).
Contact us
Join the community
to join the community
Try Guance
Start online and pay only for what you use.
Get startedChoose a Guance plan