httpx AsyncClient connection pool and Limits (tested)
Create one httpx.AsyncClient and reuse it: 20 requests used 1 TCP connection instead of 20. The default pool allows 100 connections and 20 idle keep-alive ones; set limits=httpx.Limits(max_connections=...) to cap concurrency.
An httpx.AsyncClient (or Client) owns a connection pool. Reuse one client for many requests and size the pool with httpx.Limits. Measured with httpx 0.28.1 on Python 3.13.5 against a local HTTP/1.1 http.server (srv.py) that records each client TCP port; /slow sleeps 1 second.
1. Reuse one AsyncClient
import asyncio, httpx
import srv
base = srv.start()
async def new_client_each_time():
for _ in range(20):
async with httpx.AsyncClient() as client:
await client.get(base + "/")
async def one_shared_client():
async with httpx.AsyncClient() as client:
for _ in range(20):
await client.get(base + "/")
for f in (new_client_each_time, one_shared_client):
srv.PORTS.clear()
asyncio.run(f())
print(f"{f.__name__}: 20 requests, {len(srv.PORTS)} TCP connections")
Output:
new_client_each_time: 20 requests, 20 TCP connections
one_shared_client: 20 requests, 1 TCP connections
A new client per request opens (and closes) a new TCP connection, plus a TLS handshake on HTTPS. Create one client at startup (for example in a FastAPI lifespan) and close it at shutdown.
2. httpx asyncclient connection pool defaults
import httpx
c = httpx.AsyncClient()
pool = c._transport._pool
print("max_connections:", pool._max_connections)
print("max_keepalive_connections:", pool._max_keepalive_connections)
print("keepalive_expiry:", pool._keepalive_expiry)
print(httpx.Limits())
Output:
max_connections: 100
max_keepalive_connections: 20
keepalive_expiry: 5.0
Limits(max_connections=None, max_keepalive_connections=None, keepalive_expiry=5.0)
A client created without limits= allows 100 connections, keeps up to 20 idle ones alive, and drops idle ones after 5 seconds. A bare httpx.Limits() means no limit (None), not the client default. (The pool attributes printed here are private, shown only to read the effective values.)
3. max_connections caps concurrency
import asyncio, time, httpx
import srv
base = srv.start()
async def run(limits):
async with httpx.AsyncClient(limits=limits, timeout=30) as client:
srv.PORTS.clear(); t = time.perf_counter()
await asyncio.gather(*(client.get(base + "/slow") for _ in range(10)))
print(f"{limits}: 10 x 1s requests in {time.perf_counter()-t:.1f}s, {len(srv.PORTS)} connections")
asyncio.run(run(httpx.Limits(max_connections=100)))
asyncio.run(run(httpx.Limits(max_connections=2)))
Output:
Limits(max_connections=100, max_keepalive_connections=None, keepalive_expiry=5.0): 10 x 1s requests in 1.0s, 10 connections
Limits(max_connections=2, max_keepalive_connections=None, keepalive_expiry=5.0): 10 x 1s requests in 5.2s, 2 connections
With max_connections=2, ten concurrent 1-second requests ran two at a time (5.2s). Extra requests wait for a free connection instead of failing, as long as the pool timeout allows.
4. PoolTimeout
import asyncio, httpx
import srv
base = srv.start()
async def main():
limits = httpx.Limits(max_connections=2)
async with httpx.AsyncClient(limits=limits, timeout=httpx.Timeout(5.0, pool=0.5)) as client:
try:
await asyncio.gather(*(client.get(base + "/slow") for _ in range(5)))
except httpx.PoolTimeout as e:
print("raised", type(e).__name__, "after waiting 0.5s for a free connection")
asyncio.run(main())
Output:
raised PoolTimeout after waiting 0.5s for a free connection
httpx.PoolTimeout means a request waited longer than the pool timeout for a connection. Raise max_connections, lower concurrency with an asyncio.Semaphore, or allow a longer pool timeout. Also check that streamed responses are closed, because an open stream keeps its connection.
Related: httpx timeout, httpx stream, httpx vs requests differences, aiohttp unclosed client session.
Frequently asked questions
What is the default connection pool size of httpx AsyncClient?
In httpx 0.28.1: max_connections=100, max_keepalive_connections=20, keepalive_expiry=5.0 seconds.
Should I create a new httpx client per request?
No. Each new client opens a new connection; one shared client reused 1 connection for 20 requests in the test.
What causes httpx.PoolTimeout?
All max_connections connections were busy for longer than the pool timeout. Increase max_connections, limit concurrency, or close streamed responses.
Written with AI assistance. Measured on 2026-10-23 with httpx 0.28.1 on Python 3.13.5 against a local HTTP/1.1 http.server that records client connections. Each block was run verbatim and the output pasted unedited. PlainHeaders is our extension; httpx is free.