Internals: httpcore and AnyIO

HTTPX · Expert · 12 min

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 h11 and HTTP/2 via h2, proxy tunnelling, socket and TLS handling. HTTPTransport and AsyncHTTPTransport are 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 1

Remember

  • 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).

Check yourself

  1. Which library actually manages connection pools and HTTP/1.1 framing under HTTPX?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.python-httpx.org/advanced/transports/