Diagnosing pool stalls, backend issues or overhead requires knowing what sits below the client API.
HTTPX is a layered design:
- httpx (the client layer): URL and header models, cookies, auth flows, redirects, event hooks, decoding, the sync and async client classes.
- httpcore (the transport layer): connection pools, HTTP/1.1 via
h11and HTTP/2 viah2, proxy tunnelling, socket and TLS handling.HTTPTransportandAsyncHTTPTransportare thin adapters that convert httpx requests into httpcore requests.
httpcore has parallel sync and async implementations of the pool and connections. The async code runs on AnyIO, so it works on asyncio and Trio; the backend is detected from the running event loop. The sync code uses blocking sockets and threads' own locks, so a sync Client can be shared between threads.
Connection pooling works per origin: a request takes an idle connection or opens one (respecting max_connections), sends with h11 or h2, and returns the connection when the response body is fully read or closed. Unclosed streamed responses therefore leak pool capacity. With HTTP/2, one connection carries many concurrent streams up to the server's limit.
Performance notes: HTTPX does more per request than raw sockets (models, header validation, hooks), which matters only at thousands of requests per second; then reuse one client, avoid per-request client creation, and consider HTTP/2 or aiohttp for extreme throughput. Enable logging with logging.getLogger("httpx") and "httpcore" at DEBUG level to see connection lifecycle events like connect_tcp, start_tls and send_request_headers.
Example
import logging
import httpx
logging.basicConfig(format="%(name)s %(message)s")
logging.getLogger("httpcore").setLevel(logging.DEBUG) # connection-level events
logging.getLogger("httpx").setLevel(logging.INFO) # one line per request
with httpx.Client(base_url="https://httpbin.org") as client:
client.get("/get") # httpcore.connection connect_tcp.started ...
client.get("/get") # no connect_tcp: the pooled connection is reused
pool = client._transport._pool # the httpcore pool
print(type(pool).__module__.split(".")[0], len(pool.connections)) # httpcore 1Remember
- httpx = client features; httpcore = pools, h11/h2, sockets
- Async runs on AnyIO, so both asyncio and Trio work
- DEBUG logging on httpcore shows connection reuse and TLS setup
Watch out for
- Relying on private attributes like client._transport in production code (fine for debugging only).