backend / web frameworks / aiohttp / 04_aiohttp_vs_httpx.md

aiohttp vs httpx

6 interview angles 5 min read source

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 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.