cheat sheet

httpx

Package-level reference for httpx on PyPI — install variants, version policy, extras, transport options, and alternatives.

httpx

What it is

httpx is a full-featured HTTP client for Python created by Tom Christie (also the author of Starlette and Django REST framework) and maintained under the encode GitHub organization. It offers a requests-compatible synchronous API plus a true async/await client built on httpcore and anyio, with optional HTTP/2 support.

Reach for httpx when you need any combination of: async I/O (inside FastAPI, Litestar, or asyncio jobs), HTTP/2, fine-grained timeout control, or a single library that works in both sync and async code paths. The sync API is close enough to requests that most code ports with import-line changes only.

Install

bash
pip install httpx

Output: (none — exits 0 on success)

bash
uv add httpx

Output: dependency resolved + added to pyproject.toml

bash
poetry add httpx

Output: updated lockfile + virtualenv install

bash
pip install "httpx[http2]"           # HTTP/2 transport via h2
pip install "httpx[socks]"           # SOCKS proxy support
pip install "httpx[cli]"             # bundled `httpx` CLI (rich-printed responses)
pip install "httpx[brotli,zstd]"     # extra response decoders

Output: core httpx plus the named optional dependencies

Versioning & Python support

  • Current line is the 0.x series (as of mid-2026). The project has not declared a 1.0 despite production-grade stability — minor releases (0.27, 0.28, …) can include small breaking changes, so pin a minor range in production.
  • Supports Python 3.9+ on recent releases.
  • The 1.0 milestone is a long-standing project goal; expect a stable API once it lands.

Package metadata

  • Maintainer: Tom Christie / encode org on GitHub
  • Project home: github.com/encode/httpx
  • Docs: python-httpx.org
  • PyPI: pypi.org/project/httpx
  • License: BSD-3-Clause
  • Governance: community-maintained, commercial sponsorship via Encode
  • First released: 2019
  • Downloads: tens of millions per month; standard async HTTP client across the FastAPI ecosystem

Optional dependencies & extras

  • httpx[http2] — installs h2 for HTTP/2 transport. Required for httpx.Client(http2=True).
  • httpx[socks] — installs socksio for SOCKS4/5 proxy URLs.
  • httpx[cli] — installs click + rich + pygments to enable the bundled httpx CLI command.
  • httpx[brotli] / httpx[zstd] — adds brotli / zstandard decoders for Content-Encoding: br and zstd responses.

Core dependencies pulled in automatically:

  • httpcore — low-level transport (connection pooling, retries)
  • anyio — async runtime adapter (asyncio / trio compatible)
  • certifi — bundled CA certificate store
  • idna — internationalized domain names
  • sniffio — detect the running async backend

Alternatives

PackageTrade-off
requestsSync-only; broader ecosystem and SDK assumptions. Use for blocking scripts and integrations.
aiohttpAsync client + server combined; older, more bespoke API. Heavier than httpx for client-only use.
urllib3Lower-level — direct connection-pool control. Use when you need wire-level fine tuning.
niquestsDrop-in requests fork with HTTP/2, HTTP/3, async. Use when you want the requests API with modern transport.
urllib (stdlib)Zero-dependency. Use only when you cannot add a third-party package.

Common gotchas

  1. Pre-1.0 versioning means minor releases can break. Pin a tight range (httpx>=0.27,<0.28) in libraries; let applications float more freely but watch the changelog.
  2. HTTP/2 is opt-in. Client(http2=True) needs the h2 extra installed AND the server to ALPN-negotiate it. Without both, you silently get HTTP/1.1.
  3. Default timeout is 5 s on all phases. Unlike requests (no default), httpx returns ReadTimeout quickly. Override with timeout=httpx.Timeout(connect=5.0, read=30.0).
  4. Client and AsyncClient are separate types. A sync Client cannot be awaited; an AsyncClient cannot be used from sync code without asyncio.run(). Be explicit about which mode each call site is in.
  5. Connection pool is shared per Client, not per host. Default pool is 10 keepalive + 100 max. Tune via httpx.Limits(max_keepalive_connections=N, max_connections=M).
  6. certifi CA bundle, not system trust store. Same caveat as requests — override with verify="/path/to/ca.pem" or set SSL_CERT_FILE.
  7. Cookie persistence differs from requests. httpx Cookies are per-Client; one-shot httpx.get() does not persist cookies.

Real-world recipes

Worked examples that target the specific value of httpx over requests — async concurrency, HTTP/2 multiplexing, and streaming. The API-style recipes (auth flow, JSON parsing) are in the companion language-section article; here we focus on transport, pool, and runtime concerns.

Recipe 1 — Reusable async client tuned for high concurrency.

python
import httpx

limits = httpx.Limits(max_keepalive_connections=20, max_connections=200, keepalive_expiry=30.0)
timeout = httpx.Timeout(connect=5.0, read=30.0, write=10.0, pool=5.0)

async def make_client() -> httpx.AsyncClient:
    return httpx.AsyncClient(
        http2=True,
        limits=limits,
        timeout=timeout,
        headers={"User-Agent": "myapp/1.0"},
        follow_redirects=True,
    )

Output: client object; reuse across the application lifetime and close on shutdown.

Recipe 2 — Concurrent fan-out with bounded parallelism.

python
import asyncio, httpx

async def fetch_all(urls):
    sem = asyncio.Semaphore(20)
    async with httpx.AsyncClient(http2=True) as client:
        async def one(u):
            async with sem:
                r = await client.get(u, timeout=10)
                return r.status_code, r.text[:200]
        return await asyncio.gather(*(one(u) for u in urls))

Output: at most 20 in-flight requests; gather returns the list of (status, body) tuples.

Recipe 3 — Streaming a large download to disk without buffering the whole body.

python
async def download(client: httpx.AsyncClient, url: str, path: str):
    async with client.stream("GET", url, timeout=httpx.Timeout(60, read=None)) as r:
        r.raise_for_status()
        with open(path, "wb") as f:
            async for chunk in r.aiter_bytes(chunk_size=64 * 1024):
                f.write(chunk)

Output: file written incrementally; memory footprint stays at chunk size regardless of file size.

Recipe 4 — Custom transport for retry-on-connect.

python
transport = httpx.AsyncHTTPTransport(retries=3, http2=True)
client = httpx.AsyncClient(transport=transport)

Output: connect-level retries (NOT response-status retries) are wired in; for status-code retries, use tenacity or stamina around the call.

Performance tuning

The biggest perf knobs are pool sizing, HTTP/2 multiplexing, and transport reuse. Misconfigured, an async client will be slower than requests because it pays the asyncio overhead without amortizing it across concurrent calls.

  • Reuse one AsyncClient per process. Per-request clients pay full TLS handshake every time; in benchmarks this is a 5-10× slowdown.
  • HTTP/2 helps when fanning out to one host. Many concurrent requests to one origin multiplex over a single TCP stream; HTTP/1.1 pipelining is unreliable in practice, so HTTP/2 is the only way to avoid head-of-line blocking. To many different hosts, HTTP/2 makes no difference — each host still gets its own connection.
  • Tune httpx.Limits to your workload. Default max_connections=100, max_keepalive_connections=20 is fine for a small worker; for an aggregator hitting many hosts, bump both. For a microservice serving a single backend, lower max_connections to match the backend's worker count.
  • keepalive_expiry controls idle-connection close. Default 5 s is short — a 30-60 s value reduces handshake churn for low-RPS services.
  • http1 and http2 toggles on Client decide ALPN negotiation. Disable HTTP/1.1 (http1=False, http2=True) only when you control the upstream.
  • Buffer reads with iter_bytes and chunk_size=64*1024 — the default chunk size is small and wastes scheduler turns on large bodies.
  • Use httpx[brotli,zstd] when targeting modern servers — better compression than gzip, less CPU on decode.

Version migration guide

httpx is still pre-1.0 and minor releases sometimes change behavior. Pin tight and read the changelog before bumping. The list below collects the gotchas that have bitten users since the 0.18 series.

  • 0.18 → 0.19Client.send() no longer auto-converts strings to URL objects in some paths; pass httpx.URL(...) explicitly.
  • 0.20 → 0.21Response.history semantics aligned with requests; redirect chains now include the final response correctly.
  • 0.22 → 0.23 — default proxies argument deprecated in favor of proxy; sync clients no longer accept a dict of proxies — pass a single URL.
  • 0.24 → 0.25Client(app=...) for ASGI/WSGI in-memory testing moved to Client(transport=httpx.ASGITransport(app=...)).
  • 0.27 → 0.28verify no longer accepts True/False strings; pass real booleans or a path. AsyncClient close semantics tightened — leaking a client now warns.
python
# Before (0.24-)
client = httpx.AsyncClient(app=my_app, base_url="http://test")

# After (0.25+)
transport = httpx.ASGITransport(app=my_app)
client = httpx.AsyncClient(transport=transport, base_url="http://test")

Output: same in-memory ASGI behavior with the new transport-explicit form.

Testing & CI integration

Async HTTP tests need either a real fake server or a transport-level mock. respx is the canonical mocking layer; ASGITransport/WSGITransport let you call your own app in-memory without a network port.

python
# pip install respx pytest-asyncio
import httpx, respx, pytest

@pytest.mark.asyncio
async def test_fetch():
    async with respx.mock(base_url="https://api.example.com") as mock:
        mock.get("/users/42").respond(200, json={"id": 42, "name": "Alice Dev"})
        async with httpx.AsyncClient(base_url="https://api.example.com") as c:
            r = await c.get("/users/42")
        assert r.json()["name"] == "Alice Dev"
        assert mock["/users/42"].call_count == 1

Output: test passes; no socket opened, route assertions enforced.

python
# In-memory ASGI testing for a FastAPI / Starlette app
import httpx
from myapp import app  # ASGI application

async def test_app():
    async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as c:
        r = await c.get("/health")
    assert r.status_code == 200

Output: the app handles the request without a uvicorn process.

Troubleshooting common errors

Error / SymptomLikely causeFix
httpx.ConnectTimeoutNetwork reach or DNS slowIncrease the connect timeout; check name resolution.
httpx.ReadTimeoutServer slow or hungIncrease read half of httpx.Timeout.
httpx.PoolTimeoutAll pool slots busyRaise Limits.max_connections, lower client-side concurrency, or set pool timeout to None.
httpx.RemoteProtocolError: Server disconnected without sending a responseServer closed mid-handshakeRetry with backoff; check upstream health.
RuntimeError: This event loop is already runningCalling asyncio.run inside a running loopUse await directly; or wrap with nest_asyncio only in Jupyter.
httpx.UnsupportedProtocol: Request URL is missing an 'http' or 'https' protocolBare path passedPrepend the scheme or set Client(base_url=...).
HTTP/2 not negotiated despite http2=Trueh2 not installedpip install "httpx[http2]".
Cookies not persisted between requestsOne-shot httpx.get() usedSwitch to a Client/AsyncClient context.
ssl.SSLCertVerificationErrorPrivate CAPass verify="/path/to/ca.pem" or set SSL_CERT_FILE.
Memory grows on long-running async workerClient never closedUse async with or call await client.aclose() on shutdown.

Security considerations

httpx shares requests' secure defaults — certifi CA bundle, redirects strip cross-host auth, TLS-1.2 minimum — but the async surface and HTTP/2 support add a few extra footguns.

  • Never verify=False in production. Same caveat as requests. Override with a CA path or SSL_CERT_FILE.
  • Auth flows. Use httpx.BasicAuth, httpx.DigestAuth, or write a custom httpx.Auth subclass. Don't manually set Authorization headers per request — easy to leak across redirects.
  • HTTP/2 server push is disabled by default in httpx; ignore the noise about push from older HTTP/2 articles.
  • Cookie handling per-Client. A one-shot httpx.get() is a fresh cookie jar; never use one-shots for authenticated flows.
  • Proxy URL secrets. Embedded credentials in proxy="http://user:pass@host:port" print in repr — load via env, strip before logging.
  • follow_redirects=False by default. httpx.get() does NOT follow redirects (requests does). Set follow_redirects=True explicitly when needed.
  • TLS context customization. Pass a pre-built ssl.SSLContext to Client(verify=ctx) for tight control (cipher list, ALPN protocols, client cert).
  • Watch for CVEs in h2 and httpcore. The HTTP/2 stack has had a small number of denial-of-service issues; pin minimum versions in production.

When NOT to use this

httpx is great, but it isn't always the right answer:

  • A tight one-shot scripturllib.request from the stdlib avoids the install. requests has more docs and community examples.
  • An existing requests-based codebase where async isn't needed — the migration cost rarely pays back. Stay on requests until something forces the switch.
  • WebSocketshttpx doesn't speak WebSockets. Use websockets, aiohttp, or the framework's own WS support.
  • HTTP/3 (QUIC)httpx does not support HTTP/3 yet. Use aioquic or niquests for QUIC.
  • Pre-1.0 stability concerns. If your platform forbids pre-1.0 dependencies, pick requests (2.x stable) or aiohttp (3.x stable).

Ecosystem integrations

httpx is the default outbound HTTP client across the modern async-Python ecosystem; the integrations below are the ones to keep on hand.

  • respx — mock HTTP at the transport layer. The canonical test double for httpx.
  • tenacity / stamina — retry decorators that wrap httpx calls with backoff and circuit breakers.
  • hishel — RFC-compliant HTTP cache layer for httpx. Drop-in CacheClient / AsyncCacheClient.
  • httpx-ws — WebSockets transport for httpx (httpx itself doesn't ship WS).
  • httpx-oauth — OAuth2 client flows on top of httpx.
  • fastapi.testclient.TestClient — wraps httpx for in-process ASGI testing.
  • opentelemetry-instrumentation-httpx — tracing.
  • sentry-sdk — automatic httpx breadcrumbs.
  • anyiohttpx.AsyncClient works under both asyncio and trio because it uses anyio internally.
  • niquests — a requests-API-compatible alternative that also offers HTTP/2 + HTTP/3; consider when migrating from requests and want familiar API.

Compatibility matrix

Pythonhttpx lineNotes
3.80.24 and earlierDrop deprecated; floor of older releases.
3.90.25+Current floor for recent minor releases.
3.100.26+Pattern matching usable in user code.
3.110.26+Best perf via asyncio improvements.
3.120.27+New asyncio task-group APIs supported.
3.130.28+Latest stable; free-threaded build untested.

Pair compatibility:

  • httpx[http2] requires h2>=4.
  • httpx[socks] requires socksio.
  • respx typically requires the same major-minor as httpx.

Production deployment notes

httpx is a library, not a server — there's no deployment artifact per se. But long-running services that use httpx heavily share a small checklist worth treating as the deployment surface.

  • One AsyncClient per process, opened in lifespan. Initialize in your framework's startup hook; close on shutdown. Per-request clients waste TLS handshakes and skew latency tails.
  • Health-check separately. Add a no-op await client.head("/health") against your most-used upstream so the keepalive pool stays warm.
  • Honor upstream timeouts. Set httpx.Timeout(connect=5, read=30, write=10, pool=5) explicitly — the 5s default read bites under upstream pressure.
  • Tune Limits to match worker concurrency. A FastAPI worker with --workers 4 --threads 1 and a high-fanout endpoint may saturate the default 100-connection pool fast.
  • Avoid httpx.get() (module-level convenience). It creates and tears down a fresh client per call. Useful for scripts; never use in a service.
  • Watch httpcore errors. Most "weird" httpx errors bubble up from httpcore; the actual cause is usually network or TLS at the OS level.
  • Containerize with a non-blocking signal handler. Long downloads can hold the event loop past SIGTERM — register a signal.signal(SIGTERM, ...) to close clients before exit.

See also