aiohttp vs httpx
Two go-to async HTTP clients for Python. Both work. Differences matter at the edges.
At a glance
| aiohttp | httpx | |
|---|---|---|
| Async support | async only | sync + async with same API |
| HTTP/2 | 4.0 beta | yes (with http2=True) |
| HTTP/3 | no | no |
| Server framework | yes (legacy-ish) | no (client-only) |
| Mocking story | OK | excellent (respx, native MockTransport) |
| Connection pool | TCPConnector | Limits / connection pool built-in |
| Streaming | yes | yes |
| WebSocket client | yes | no (use websockets or other) |
| Default timeout | 5 min total | 5s (more conservative) |
| First release | 2014 | 2019 |
| Maturity / ecosystem | mature, older | younger, modern |
| Speed (raw) | slightly faster on benchmarks | very close |
API comparison
Simple GET
# aiohttp
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
data = await resp.json()
# httpx
async with httpx.AsyncClient() as client:
resp = await client.get(url)
data = resp.json()
httpx returns a Response object directly — no inner context manager. Sync version is identical except for await.
Sync httpx
with httpx.Client() as client:
resp = client.get(url)
data = resp.json()
aiohttp has no sync mode. If your codebase mixes sync and async, httpx is the single-library answer.
Timeouts
# aiohttp
timeout = aiohttp.ClientTimeout(total=10, connect=2, sock_read=5)
session = aiohttp.ClientSession(timeout=timeout)
# httpx
timeout = httpx.Timeout(timeout=10.0, connect=2.0, read=5.0, write=5.0, pool=2.0)
client = httpx.AsyncClient(timeout=timeout)
Similar semantics. httpx has pool (wait for a free connection); aiohttp lumps this into connect.
Connection limits
# aiohttp
connector = aiohttp.TCPConnector(limit=100, limit_per_host=30)
session = aiohttp.ClientSession(connector=connector)
# httpx
limits = httpx.Limits(max_connections=100, max_keepalive_connections=30, keepalive_expiry=15)
client = httpx.AsyncClient(limits=limits)
Slightly different naming. Both end up at the same place.
When httpx wins
1. Sync + async same API
# Single client class, used in both contexts
class Client:
def __init__(self):
self.sync = httpx.Client(...)
self.async_ = httpx.AsyncClient(...)
For libraries supporting both, httpx is the right base.
2. HTTP/2
client = httpx.AsyncClient(http2=True)
Multiplexing one TCP connection across many requests. Big latency win when calling the same server many times.
3. Testing / mocking
# httpx native MockTransport
transport = httpx.MockTransport(lambda req: httpx.Response(200, json={"ok": True}))
client = httpx.AsyncClient(transport=transport)
# Or via respx (the de-facto httpx mock library)
import respx
@respx.mock
async def test_foo():
respx.get("https://api.example.com/data").respond(200, json={"items": []})
async with httpx.AsyncClient() as client:
resp = await client.get("https://api.example.com/data")
aiohttp testing is doable (aioresponses, aiohttp-test-utils) but more friction.
4. Pydantic / typed integration
Many modern API clients (OpenAI, Anthropic SDKs, etc.) use httpx internally. Integrating with them is easier when your code also uses httpx.
When aiohttp wins
1. WebSocket client + HTTP client in one library
aiohttp has both. httpx doesn’t include WebSocket — you’d add websockets or aiohttp-ws.
2. Existing aiohttp codebase
Migrations cost time. Don’t switch just because.
3. Raw throughput
aiohttp is slightly faster on benchmarks for high-concurrency throughput. In practice, downstream latency dominates; the library overhead is negligible.
4. Server framework
aiohttp’s web framework is a real thing. httpx is client-only.
httpx idioms aiohttp lacks
Native retry transport (via httpx-retries or wrappers)
# httpx-retries or stamina
from stamina import retry
@retry(on=httpx.RequestError, attempts=3)
async def fetch(client, url):
resp = await client.get(url)
resp.raise_for_status()
return resp.json()
Both libraries lack built-in retries; same workaround applies.
follow_redirects=False by default
# httpx default: don't follow redirects
client = httpx.AsyncClient(follow_redirects=True) # explicit opt-in
# aiohttp default: follows redirects
session = aiohttp.ClientSession()
# explicit: session.get(url, allow_redirects=False)
Different defaults. httpx’s “don’t follow by default” is the safer choice for API clients (preserves status codes; doesn’t silently rewrite POST → GET on 301).
raise_for_status on Response
resp = await client.get(url)
resp.raise_for_status() # raises HTTPStatusError on 4xx/5xx
aiohttp equivalent:
async with session.get(url) as resp:
resp.raise_for_status() # also works
Both have it; httpx’s is slightly cleaner because there’s no async with wrapping.
Migration tips
aiohttp → httpx is mostly mechanical:
aiohttp.ClientSession()→httpx.AsyncClient().async with session.get(url) as resp:→resp = await client.get(url).await resp.json()→resp.json()(sync attribute, since body is buffered).ClientTimeout(...)→httpx.Timeout(...).TCPConnector(...)→httpx.Limits(...).
Streaming differs:
aiohttp:async for chunk in resp.content.iter_chunked(N).httpx:async with client.stream("GET", url) as resp: async for chunk in resp.aiter_bytes(N):.
The stream context manager is httpx-specific; without it, the whole body is buffered.
Performance practical note
Both are fast enough that the library choice rarely matters for service throughput. What matters:
- Reusing sessions/clients (not creating per request).
- Setting reasonable connection limits.
- Reasonable timeouts.
- Retry / circuit-breaker discipline.
Get those right with either library and you’ll be fine.
Recommendation
| If… | Pick |
|---|---|
| New project, any size | httpx |
| Existing aiohttp codebase | stay with aiohttp |
| Need sync + async in one library | httpx |
| Need HTTP/2 | httpx |
| Need WebSocket client in same library | aiohttp (or websockets lib + httpx) |
| Building a library (one HTTP client, two contexts) | httpx |
| Heavy mocking in tests | httpx (respx) |
| Raw max throughput, every microsecond matters | aiohttp marginally faster |
For most Python backend roles in 2025: httpx is the modern default. aiohttp remains common in legacy and async-heavy systems.
Interview angle
- “aiohttp vs httpx — which would you pick for a new project?” — httpx in most cases: sync+async same API, HTTP/2, better mocking story, cleaner ergonomics. aiohttp if you specifically need its server framework or WebSocket client built-in.
- “What’s the HTTP/2 win?” — multiplexing many requests over one TCP connection. No head-of-line blocking at the connection level. Beneficial when calling the same server frequently. httpx supports it (
http2=True); aiohttp 3.x doesn’t. - “How does test mocking differ?” — httpx has
MockTransportnatively, plusrespxis well-maintained and idiomatic. aiohttp hasaioresponsesandaiohttp-test-utilsbut more friction. For test-heavy code, httpx is easier. - “What’s the same in both?” — pooled connections, async semantics, timeouts, streaming, basic concurrency control. Performance is comparable in real-world use.
- “Why pick aiohttp despite httpx being newer?” — existing codebase, you also need a WebSocket client in the same library, or you’re building an async-only service framework (aiohttp.web).
- “Default behavior differences?” — aiohttp follows redirects by default; httpx doesn’t. aiohttp’s default timeout is 5 minutes; httpx’s is 5 seconds. Always set timeouts explicitly to avoid surprises.