httpx review
HTTPX 0.28.1 is a Python HTTP client with closely matched synchronous and asynchronous interfaces. It uses Client and AsyncClient for connection pooling, supports HTTP/1.1 and optional HTTP/2, applies timeouts by default, and can call ASGI or WSGI applications through in-process transports. The request and response style is intentionally familiar to Requests users, but the transport model also exposes pool limits, proxies, streaming, custom authentication flows, and mock transports. HTTP/2 support requires an extra install and an explicit client option.
HTTPX is the strongest general choice for async Python calls and in-process ASGI tests. Pin 0.28.x carefully and add deliberate response-retry policy when the service contract needs it.
We installed it
| Install | ✓ · 0.2s | 7 packages on disk · 3 MB |
| Import | ✓ | import httpx in 0.37s · pure Python · py.typed · requires Python >=3.8 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does httpx install cleanly?
Yes. In a fresh container with an empty cache, pip install httpx finished in 0.2s, leaving 7 packages and 3 MB on disk. pip-audit reported no known vulnerabilities.
What does httpx need to run?
Python >=3.8, and nothing compiled: it is pure Python. In our run import httpx succeeded in 0.37s, and the package ships py.typed for type checkers.
httpx or requests: which should you use?
requests: Use it for synchronous applications that value a settled API and the widest existing examples. HTTPX is the strongest general choice for async Python calls and in-process ASGI tests.
When should you not use httpx?
A stable 1.x compatibility promise is mandatory; the project remains on 0.28 and has removed deprecated arguments in minor releases
Discussed on
- hnHttpx: A next-generation HTTP client for Python463 points
- hnWhy I forked httpx252 points
- hnHttpx: A Ruby HTTP library157 points
Use it if
- An asyncio application needs an awaitable client rather than moving Requests calls into threads
- Tests should call an ASGI application in-process without listening on a network port
- One codebase needs similar sync and async request APIs with typed package declarations
- Pool timeouts, transport replacement, or optional HTTP/2 need to be configured directly
- A stable 1.x compatibility promise is mandatory; the project remains on 0.28 and has removed deprecated arguments in minor releases
- Automatic retries for status codes and backoff must be built in; transport retries cover connection establishment only
- A synchronous script already uses Requests and gains nothing from async, HTTP/2, or custom transports
- Peak concurrent-client throughput matters more than API similarity; aiohttp may fit that narrower workload better
- The application needs both client and server implementation from one project; HTTPX is client-side
Setup reality
HTTPX 0.28.1 installed in 0.2 seconds in our fresh Python 3.12 environment. Seven packages used 3 MB on disk. The distribution declares 12 direct dependencies, is pure Python, supports Python 3.8 or newer, uses BSD-3-Clause, and ships py.typed. pip-audit reported no known vulnerabilities. import httpx completed in 0.37 seconds.
Top-level httpx.get() opens a short-lived connection context. Reuse Client or AsyncClient for repeated calls so pooling can do its job, and close it through a context manager. Do not create an AsyncClient inside a hot loop. The default timeout applies separate connect, read, write, and pool phases; tune them rather than passing one huge number or disabling every timeout.
HTTP/2 needs httpx[http2] plus http2=True, and the server must negotiate it. SOCKS support is another extra. In 0.28, use proxy= for a single proxy or mounts= for per-scheme routing; older examples using proxies= are stale. TLS verification is on by default. Supply a documented SSLContext when custom trust roots are required instead of setting verify=False.
Async streaming must remain inside the response context or be closed manually. ASGITransport calls the app but does not run lifespan events for it, so tests needing startup and shutdown should add a lifespan manager. HTTPTransport(retries=N) retries connect failures and connect timeouts, not 429 or 503 responses. Application-level retry code must restrict methods, honor Retry-After when relevant, and bound the total deadline.
Patterns
Fetch JSON and reject an HTTP error simple-get
import httpx
r = httpx.get('https://api.example.com/items', params={'page': 2})
r.raise_for_status()
data = r.json()raise_for_status separates unsuccessful HTTP responses from successful responses before JSON parsing.
Reuse one pool across related calls client-reuse
import httpx
with httpx.Client(base_url='https://api.example.com',
headers={'Authorization': f'Bearer {token}'}) as client:
a = client.get('/users/1')
b = client.get('/users/2')A Client keeps connections and shared headers. Close it with a context manager instead of constructing one per request.
Send concurrent requests with AsyncClient async-requests
import asyncio
import httpx
async def main():
async with httpx.AsyncClient() as client:
results = await asyncio.gather(
client.get('https://api.example.com/a'),
client.get('https://api.example.com/b'),
)
return [r.json() for r in results]
asyncio.run(main())Create the async client outside repeated tasks and close it after gathered work completes.
Send a JSON request body post-json
r = client.post('https://api.example.com/items',
json={'name': 'widget', 'qty': 3})
r.raise_for_status()json sets serialization and content type. raise_for_status is still needed if non-2xx responses should become exceptions.
Give each timeout phase a budget timeouts
timeout = httpx.Timeout(10.0, connect=5.0)
client = httpx.Client(timeout=timeout)
# or disable for a long streaming call
client.get(url, timeout=None)HTTPX has connect, read, write, and pool timeouts. timeout=None removes protection and should be limited to a known streaming case.
Separate timeout, network, and status failures error-handling
try:
r = client.get(url)
r.raise_for_status()
except httpx.TimeoutException:
... # connect/read/write/pool timeout
except httpx.HTTPStatusError as e:
print(e.response.status_code, e.response.text)
except httpx.RequestError as e:
print(f'network problem talking to {e.request.url}')HTTPStatusError has a response. RequestError covers transport failures, while timeout subclasses identify the exhausted phase.
Stream a download without buffering it all streaming-download
with client.stream('GET', 'https://example.com/big.iso') as r:
r.raise_for_status()
with open('big.iso', 'wb') as f:
for chunk in r.iter_bytes(chunk_size=65536):
f.write(chunk)Keep iteration inside the stream context so the response closes and returns its connection to the pool.
Retry connection establishment connect-retries
transport = httpx.HTTPTransport(retries=3)
client = httpx.Client(transport=transport)Transport retries do not retry 429, 500, or other completed HTTP responses. Add bounded application policy for those.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| requests | PyPI | Use it for synchronous applications that value a settled API and the widest existing examples. |
| aiohttp | PyPI | Use it for high-concurrency asyncio workloads or when the same project must also provide a server. |
| urllib3 | PyPI | Use it for lower-level pooling and its configurable Retry behavior without a session-style facade. |
More web backend guides
urllib3 · requests · ws · anyio · undici · express · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

