HTTPX is a Python HTTP client with synchronous and asynchronous APIs. Reliable use is less about calling get() and more about setting timeouts, reusing connections, limiting responses, and translating remote failures into useful application errors.
Reuse the client
import httpx
timeout = httpx.Timeout(5.0, connect=2.0)
limits = httpx.Limits(max_connections=20, max_keepalive_connections=10)
with httpx.Client(timeout=timeout, limits=limits) as client:
response = client.get("https://api.example.com/status")
response.raise_for_status()
data = response.json()
Client maintains a connection pool, avoiding a fresh connection for every call. Set a client-level timeout and override it only when an endpoint has a reason. raise_for_status() handles error statuses, but returned JSON still needs validation before entering your domain.
For asynchronous code, use AsyncClient inside async with and await operations. Do not place a blocking HTTP call inside the event loop. The Python async and await guide explains that execution model.
Handle TimeoutException, NetworkError, and HTTPStatusError separately when retry policy depends on the cause. Never retry non-idempotent operations automatically without an idempotency key or equivalent guarantee. Limit streamed or buffered content when the server is untrusted.
The official HTTPX documentation, accessed July 22, 2026, covers clients, timeouts, resource limits, streaming, async support, and optional HTTP/2.
Centralize the remote API boundary
Configure base_url, authentication, headers, timeouts, and limits once. This makes the remote service an explicit dependency and keeps tests predictable. Do not commit tokens or place them in URLs, where logs and proxies may expose them.
def build_client(token: str) -> httpx.Client:
return httpx.Client(
base_url="https://api.example.com/v1/",
headers={"Authorization": f"Bearer {token}"},
timeout=httpx.Timeout(10.0, connect=2.0),
limits=httpx.Limits(max_connections=20),
)
Test the final URL because a leading slash in a request path can change its combination with base_url.
Understand timeout phases
HTTPX distinguishes connect, read, write, and pool timeouts. Read timeout limits waiting for response data, not the total duration of a large download. Pool timeout limits waiting for a reusable connection. Catch errors only where the application can make a useful decision:
def fetch_user(client: httpx.Client, user_id: int) -> dict:
try:
response = client.get(f"users/{user_id}")
response.raise_for_status()
except httpx.TimeoutException as exc:
raise RuntimeError("user service timed out") from exc
except httpx.HTTPStatusError as exc:
if exc.response.status_code == 404:
raise LookupError(user_id) from exc
raise RuntimeError("user service rejected the request") from exc
payload = response.json()
if not isinstance(payload, dict) or "id" not in payload:
raise RuntimeError("invalid user payload")
return payload
Do not return raw remote errors to users. They can disclose internal data. Log status and request identifiers while redacting authorization and personal information.
Stream with a size limit
.content buffers the body. For downloads, use stream() and count bytes. Content-Length alone is insufficient because it may be absent or false.
MAX_BYTES = 5_000_000
with httpx.Client(timeout=10.0) as client:
with client.stream("GET", "https://example.com/file.bin") as response:
response.raise_for_status()
received = 0
with open("file.bin", "wb") as output:
for chunk in response.iter_bytes():
received += len(chunk)
if received > MAX_BYTES:
raise ValueError("download exceeds size limit")
output.write(chunk)
Choose filenames yourself. A temporary file moved after validation prevents incomplete downloads from appearing finished.
Async lifetime and concurrency
Keep one AsyncClient for a service component rather than creating it inside a hot loop. Close it during shutdown. Bound task concurrency because queued coroutines and parsed bodies also consume memory. Do not share a client across unrelated event loops. In synchronous programs, prefer Client instead of wrapping every request in asyncio.run().
Retry according to HTTP semantics
Retries can help transient connection failures and selected server errors, but multiply load. Use few attempts, exponential backoff, jitter, and an overall deadline. Honor Retry-After. GET and HEAD are commonly retryable. A POST that charges a card is not automatically safe; use a server-supported idempotency key where available. Authentication and validation errors need correction, not retry.
Redirect following is deliberate. For server-side URLs influenced by users, validate destinations and repeat checks after redirects to reduce server-side request forgery risk. Keep TLS verification enabled. A managed custom CA bundle can be valid; verify=False is not a production solution.
Test without the public network
MockTransport can return controlled responses while exercising URL construction and parsing. Test success, malformed JSON, empty bodies, redirects, timeouts, and every status mapped to a domain outcome. Assert secrets never appear in logs or exceptions.
HTTP/2 support is optional and requires the matching installation extras. Multiplexing does not remove timeouts or connection limits. Keep a thin client layer responsible for HTTP details and return validated domain values to the application.
Build requests and encode data correctly
Use params= for query parameters, json= for JSON documents, data= for form fields, and files= for multipart uploads. This lets HTTPX select appropriate encoding and headers. Avoid assembling query strings manually because escaping, repeated parameters, and Unicode are easy to mishandle. Do not set Content-Type manually when HTTPX must generate a multipart boundary.
Response success is more than a 2xx status. Confirm the expected media type when appropriate, parse the body, and validate required fields and types. A server can return an HTML login page with status 200 after a proxy misconfiguration. Keep the validation close to the client boundary so invalid remote data never masquerades as a domain object.
Client ownership in web applications
Create a client during application startup and close it during shutdown. In dependency-injection frameworks, make that lifetime explicit. Creating one client per incoming request discards connection reuse, while a module-level client with no cleanup makes tests and shutdown harder. A factory plus an owned service object usually provides a clean compromise.
Cookies persist inside a client. That is useful for a deliberate session but dangerous if one shared client mixes unrelated users. Prefer token-based per-request authorization headers or isolated clients when cookie state belongs to a user. Likewise, review default headers before forwarding a request between services.
Uploads and request-body limits
Open upload files with a context manager and close them after the request. For very large or generated bodies, use streaming interfaces rather than constructing one giant bytes object. The remote server may reject a body after it has been transmitted, so local validation of type and size saves bandwidth. Never trust a browser-provided filename as a filesystem destination.
When sending JSON, distinguish omitted fields from explicit null if the API does. Serialize supported values only and handle date, decimal, or custom object conversion in the domain layer. A clear request schema prevents accidental exposure of internal attributes.
Event hooks and sensitive logging
HTTPX event hooks can add tracing or diagnostics, but hooks execute as part of request handling and should remain fast. Redact Authorization, cookies, API keys, and sensitive query values. Limit captured bodies and avoid recording personal data. Generate or propagate a correlation header according to the remote service contract, not by mutating global headers during concurrent calls.
Observability should distinguish DNS or connect failure, pool exhaustion, read timeout, HTTP rejection, decoding failure, and schema failure. These categories lead to different fixes. Metrics need bounded labels; use route templates or service names instead of complete URLs.
Mock at the transport boundary
A transport-level fake verifies method, URL, headers, and body without opening a socket. Keep a few integration tests against a controlled test server to cover streaming and protocol behavior. Do not make the normal test suite depend on a public API whose data, availability, or rate limits can change.
Test client cleanup as well as responses. Async tests should close AsyncClient, and streaming tests should exit the response context even on parse errors. Deliberately cover a body that exceeds the limit and a slow response that triggers the configured phase timeout.
Operational checklist
Before deployment, confirm a finite timeout, bounded connections, deliberate redirects, TLS verification, safe secret handling, validated responses, and an explicit client lifetime. Document which operations can retry and which require idempotency protection. Ensure streamed data has a byte limit and a safe destination.
These decisions matter more than whether the first example uses get() or post(). A reliable HTTP client treats the network as slow, fallible, and potentially hostile while giving the rest of the program a small, predictable interface.