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.