Async HTTP with httpx/aiohttp
httpx (recommended for FastAPI-era stacks) and aiohttp provide async HTTP clients with connection pooling, timeouts, and concurrent requests. Share one client instance per app lifecycle - not per request.
Recipe
import asyncio
import httpx
async def main() -> None:
async with httpx.AsyncClient(timeout=10.0) as client:
r = await client.get("https://httpbin.org/get")
print(r.status_code)
asyncio.run(main())When to reach for this:
- Fan-out API aggregation in FastAPI routes
- Webhook delivery and retries
- Service-to-service calls under load
- Streaming downloads/uploads
- Replacing blocking
requestsin async apps
Working Example
import asyncio
import httpx
URLS = [
"https://httpbin.org/delay/0.1",
"https://httpbin.org/status/200",
"https://httpbin.org/uuid",
]
async def fetch_all(client: httpx.AsyncClient, urls: list[str]) -> list[int]:
async def one(url: str) -> int:
resp = await client.get(url)
resp.raise_for_status()
return resp.status_code
return await asyncio.gather(*(one(u) for u in urls))
async def main() -> None:
limits = httpx.Limits(max_connections=10, max_keepalive_connections=5)
async with httpx.AsyncClient(timeout=5.0, limits=limits) as client:
codes = await fetch_all(client, URLS)
print(codes)
asyncio.run(main())What this demonstrates:
- Shared
AsyncClientreuses TCP connections (keep-alive) Limitscaps concurrent connections per hostraise_for_statusturns 4xx/5xx into exceptionsgatheroverlaps network waits on one loop
Deep Dive
httpx vs aiohttp
| httpx | aiohttp | |
|---|---|---|
| API style | requests-like | ClientSession + context |
| HTTP/2 | Optional extra | Server focus strong |
| FastAPI ecosystem | Common choice | Mature, large |
Client Lifecycle
- Create at app startup (FastAPI lifespan)
- Close on shutdown to flush pools
- One client per event loop thread
Gotchas
- New client per request - destroys pooling, exhausts sockets. Fix: app-scoped singleton.
- No timeouts - hung coroutines forever. Fix: default timeout on client and per-call override.
- Sync httpx in async route - blocks loop. Fix:
AsyncClientonly in coroutines. - SSL/env proxy misconfig - works in dev, fails in prod. Fix: explicit
trust_envand certs. - Unbounded gather to slow APIs - overload peer and self. Fix: semaphore limit concurrency.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| requests + to_thread | Legacy sync only | Native async route |
| urllib3 low-level | Custom protocol needs | Normal REST |
| aiohttp server | Building async web server | Client-only needs |
FAQs
httpx or aiohttp for new code?
httpx for requests-familiar API and FastAPI adjacency; aiohttp if already invested or need specific server features.
How many connections?
Match upstream rate limits and your concurrency target - often 10-100 per host with semaphore.
Retries?
httpx does not auto-retry - use tenacity or custom middleware for idempotent GET.
HTTP/2?
httpx[http2] extra - verify server support before enabling.
Streaming responses?
async with client.stream("GET", url) as resp: and aiter_bytes().
Test without network?
httpx.MockTransport or respx pytest plugin.
Share client across workers?
Each uvicorn worker process owns its own client - not cross-process shared.
Connection errors?
Retry transient; log exc.request.url - wrap DNS/TLS failures at service boundary.
aiohttp ClientSession reuse?
Same rule - one session per app lifespan, not per coroutine call.
Does Django use this?
Django async views can use httpx; majority sync stack uses requests/urllib.
Related
- Tasks & Gather - concurrent fetches
- Sync/Async Bridging - avoid sync requests
- Retries & Backoff - retry policy
- Asyncio Basics - loop fundamentals
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.