← Python Code Async Python
Browse Python Concepts

httpx AsyncClient — Practical Async HTTP in Python

Mental Model

Think of httpx.AsyncClient as a well-maintained, long-distance courier vehicle. If you buy a brand new vehicle and register it for every single delivery, you'll spend most of your time on setup and very little on actual deliveries. Instead, you should keep one vehicle running for all your deliveries.

Rule: Always share a single, long-lived httpx.AsyncClient instance across your application lifecycle rather than creating one per request.

The Setup

A DevOps engineer designs an API monitoring service that pings 50 distinct health endpoints every 10 seconds, but observes the server quickly running out of file descriptors and suffering high latency.

What Does This Print?

Broken code
Python
import asyncio
import httpx
import time

async def fetch_endpoint(url):
    # CRITICAL ANTI-PATTERN: Instantiating client on every single request
    async with httpx.AsyncClient() as client:
        response = await client.get(url)
        return response.status_code

async def main():
    urls = ["https://httpbin.org/status/200"] * 10
    start = time.perf_counter()
    
    tasks = [fetch_endpoint(url) for url in urls]
    results = await asyncio.gather(*tasks)
    
    print(f"Fetched {len(results)} pages in {time.perf_counter() - start:.2f}s")

asyncio.run(main())
Predict whether reusing an AsyncClient instance improves performance over instantiating a client per call, and what resource issues the current code causes under load.

The Output

What actually happens
Fetched 10 pages in 3.45s (Dependent on network overhead and SSL handshake times)

The code completes slowly because it establishes a completely new TCP connection and negotiates a new SSL TLS handshake for every single request. Under load, this leaves thousands of sockets in the TIME_WAIT state, leading to file descriptor exhaustion (socket leaks) on the host machine.

Why Python Does This

httpx.AsyncClient is designed to manage an internal TCP connection pool. When you execute HTTP requests inside an async with httpx.AsyncClient() block, the client retains established TCP connections and reuses them for subsequent requests to the same host (using HTTP Keep-Alive). If you construct and destroy a client on every call, the socket is immediately closed after the request, forcing the operating system to negotiate new connections from scratch. This introduces significant network and CPU overhead due to redundant TCP handshakes, TLS handshakes, and DNS resolutions.

The Fix

Corrected pattern
Python
import asyncio
import httpx
import time

# Accept the shared, thread-safe AsyncClient as an argument
async def fetch_endpoint(client, url):
    response = await client.get(url)
    return response.status_code

async def main():
    urls = ["https://httpbin.org/status/200"] * 10
    start = time.perf_counter()
    
    # Correct: Instantiate a single Client to share across all requests
    async with httpx.AsyncClient() as client:
        tasks = [fetch_endpoint(client, url) for url in urls]
        results = await asyncio.gather(*tasks)
    
    print(f"Fetched {len(results)} pages in {time.perf_counter() - start:.2f}s")

asyncio.run(main())

Reusing a single httpx.AsyncClient instance allows it to maintain an internal connection pool, keeping TCP connections alive and reusing them for subsequent requests to the same host. This significantly reduces network overhead by avoiding repeated connection establishments and SSL/TLS handshakes, improving performance.

How This Fails in Real Systems

A microservice built to aggregate pricing data from various flight APIs crashed every few minutes. Under traffic peaks, the error logs showed OSError: [Errno 24] Too many open files. The team discovered that each outgoing API request spawned a new httpx.AsyncClient. This exhausted the host's socket resources. The issue was resolved immediately by defining a single, global httpx.AsyncClient inside a FastAPI lifecycle context manager and injecting it into the endpoints.

Key Takeaway

Always share a single, long-lived httpx.AsyncClient instance across your application lifecycle rather than creating one per request.
Common mistake: Developers create a new httpx.AsyncClient instance for every HTTP request, leading to unnecessary overhead from establishing new TCP connections and SSL/TLS handshakes repeatedly.