Backend / Web frameworks / aiohttp / 04_aiohttp_vs_httpx.md

aiohttp vs httpx

Updated 6 interview angles 5 min read source
On this page9
  1. At a glance
  2. API comparison
  3. When httpx wins
  4. When aiohttp wins
  5. httpx idioms aiohttp lacks
  6. Migration tips
  7. Performance practical note
  8. Recommendation
  9. Interview angle

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

python
# 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

python
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

python
# 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

python
# 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

python
# 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

python
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

python
# 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)

python
# 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

python
# httpx default: don't follow redirects
# explicit opt-in
client = httpx.AsyncClient(follow_redirects=True)

# 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

python
resp = await client.get(url)
# raises HTTPStatusError on 4xx/5xx
resp.raise_for_status()

aiohttp equivalent:

python
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 6

  • “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 MockTransport natively, plus respx is well-maintained and idiomatic. aiohttp has aioresponses and aiohttp-test-utils but 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.