TL;DR

For async code that makes a lot of requests, aiohttp is still the fastest Python HTTP client by a wide margin. In the benchmark below it served 9x the throughput of httpx’s AsyncClient at 10 concurrent requests and 19x at 500. For synchronous code, httpx beats requests (36% more requests per second here), and requests still has no default timeout, so a stalled server can hang your process indefinitely. The bigger 2026 change is about maintenance: httpx hasn’t shipped a stable release since December 2024, Pydantic forked it as httpx2 in May, and both the OpenAI and Anthropic Python SDKs now depend on httpx2 instead of httpx. If you like the httpx API, move to httpx2, and for heavy async fan-out put it on the aiohttp transport, which ran 4.2x faster than httpx2’s default transport in my test.

FactValueSourceVerified
httpx latest stable0.28.1 (Dec 6, 2024); 1.0.dev6 pre-release Aug 31, 2026PyPI: httpxNov 5, 2026
httpx2 latest2.16.0 (Oct 21, 2026); first release 2.0.0 on May 12, 2026PyPI: httpx2Nov 5, 2026
aiohttp latest3.14.5 (Oct 11, 2026)PyPI: aiohttpNov 5, 2026
requests latest2.34.2 (May 14, 2026)PyPI: requestsNov 5, 2026
OpenAI SDK default clienthttpx2 since openai 3.0.0 (Aug 12, 2026)openai-python CHANGELOGNov 5, 2026
Anthropic SDK default clienthttpx2 since anthropic 1.0.0 (Aug 20, 2026)anthropic-sdk-python CHANGELOGNov 5, 2026

What changed in Python HTTP clients this year

For most of the 2020s the advice went like this: requests for scripts, httpx if you want async plus a requests-style API, aiohttp if you want raw async speed. That advice still holds for performance, but httpx itself stalled, and its API now lives on under a different maintainer.

httpx’s last stable release, 0.28.1, came out on December 6, 2024. In February 2026 its maintainer closed issues and discussions to anyone outside the collaborator list. Development builds of httpx 1.0, first uploaded in mid-2025, resumed in August 2026 (1.0.dev4 through dev6), but the PyPI page for those builds describes the redesign as “in early development and not yet public,” so there’s nothing to migrate to yet.

Pydantic stepped in on May 11, 2026 with httpx2, a fork of httpx 0.28.1 (and of its transport layer, httpcore 1.0.9, renamed httpcore2). The README is direct about the reason: “With HTTPX itself seeing limited activity recently, Pydantic is picking up stewardship under the HTTPX2 name so that users have a reliably maintained path forward - including timely security updates.” By October 21 it had shipped 20 stable releases, adding WebSockets, server-sent events, the QUERY method, connection-pool rewrites, and OS trust store support.

The large SDKs followed fast. openai-python 3.0.0 (August 12) made httpx2 the default, and its breaking-change note says “httpx is no longer installed automatically.” anthropic-sdk-python 1.0.0 did the same eight days later. FastAPI 0.162 and Starlette 1.10 list both httpx and httpx2 in their optional extras. If you pip install openai today, you get httpx2 whether you asked for it or not.

aiohttp and requests went on as before: aiohttp 3.14.0 shipped June 1 with a reworked parser and chunked decompression (CHANGES), and requests 2.34.0 added inline type hints in May (HISTORY).

How I benchmarked them

Every number in this section comes from runs I did for this post on November 5, 2026: CPython 3.12.3 on a 2-vCPU Linux VM with OpenSSL 3.0.13. The client libraries were httpx 0.28.1, httpx2 2.16.0 (plus httpcore2 2.16.0), requests 2.33.1 with urllib3 2.6.3, and aiohttp 3.13.5 with its C parser. httpx and httpx2 are the current PyPI releases; requests and aiohttp are one minor version behind, because the sandbox ships them preinstalled and I couldn’t upgrade.

The server is a bare asyncio protocol on uvloop that answers every request with a canned keep-alive response. It does almost no work, so the client library dominates the cost:

import asyncio, json, uvloop

SMALL = json.dumps({"id": 42, "name": "widget", "tags": ["a", "b", "c"],
                    "price": 19.99, "in_stock": True, "description": "x" * 80}).encode()

RESP = (b"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n"
        b"Content-Length: %d\r\nConnection: keep-alive\r\n\r\n%s" % (len(SMALL), SMALL))


class Proto(asyncio.Protocol):
    def connection_made(self, transport):
        self.t, self.buf = transport, b""

    def data_received(self, data):
        self.buf += data
        while (i := self.buf.find(b"\r\n\r\n")) >= 0:
            self.buf = self.buf[i + 4:]
            self.t.write(RESP)


async def main():
    srv = await asyncio.get_running_loop().create_server(Proto, "127.0.0.1", 8765, backlog=4096)
    async with srv:
        await srv.serve_forever()

uvloop.run(main())

The real server also serves a 94 KB /big response and counts TCP connections. Each client then ran in its own fresh process. Every async scenario sends 5,000 GETs behind an asyncio.Semaphore(conc), with the client’s connection pool sized to the same conc, so no library gets more sockets than another. Here’s the core of the harness (excerpted; LIB, CONC and URL come from the command line):

async def make():
    if LIB == "aiohttp":
        return aiohttp.ClientSession(connector=aiohttp.TCPConnector(limit=CONC))
    limits = lib.Limits(max_connections=CONC, max_keepalive_connections=CONC)
    return lib.AsyncClient(limits=limits)       # httpx or httpx2


async def fetch(session):
    if LIB == "aiohttp":
        async with session.get(URL) as r:
            await r.read()                       # read the body, same as httpx does
    else:
        r = await session.get(URL)               # httpx reads the full body by default
    # every response is asserted to be 200


async def batch(session, n):
    sem = asyncio.Semaphore(CONC)

    async def one():
        async with sem:
            await fetch(session)

    await asyncio.gather(*(one() for _ in range(n)))

Each run starts with a 200-request warm-up, and the figures are the median of 5 timed batches (3 at concurrency 500). Client and server share two cores, so absolute throughput is lower than on a real machine. The ratios between libraries, and the client CPU time per request, are the numbers to take away.

Sync results: requests vs httpx vs httpx2

Sequential requests over one keep-alive connection, requests.Session against httpx.Client and httpx2.Client:

ClientSmall JSON (3,000 req)94 KB body (500 req)Client CPU per small request
requests 2.33.1828 req/s668 req/s1,106 µs
httpx 0.28.11,130 req/s850 req/s797 µs
httpx2 2.16.01,247 req/s986 req/s750 µs

requests came last in both columns. httpx did 36% more small requests per second, and httpx2 another 10% on top of that. The gap is real but small in absolute terms: about 0.3 ms of client overhead per call. Across 5 million calls that adds up to roughly half an hour of CPU; across 50, it’s invisible.

The sync clients differ most in their defaults. This script asks each one what it does when you don’t configure anything:

requests 2.33.1
  default timeout: none set (Session.request timeout default = None )
  default pool:    10 host pools, 10 conns per host
  max_redirects:   30
httpx 0.28.1
  default timeout: Timeout(timeout=5.0)
  default pool:    100 max conns, 20 keepalive
  follow_redirects: False
  ssl context:     ssl SSLContext
httpx2 2.16.0
  default timeout: Timeout(timeout=5.0)
  default pool:    100 max conns, 20 keepalive
  follow_redirects: False
  ssl context:     ssl SSLContext
  has sse: True | has query: True | alias_httpx: True
  after alias_httpx(): import httpx -> httpx2 2.16.0
aiohttp 3.13.5
  default timeout: ClientTimeout(total=300, connect=None, sock_read=None, sock_connect=30, ceil_threshold=5)
  default pool:    100 total, 0 per host (0 = unlimited)

The timeout = None in requests has a visible effect. Pointed at a socket that accepts the connection and never replies, with a watchdog thread giving up after 8 seconds:

requests.get(url)                  still waiting after 8s (no default timeout)
requests.get(url, timeout=3)       ReadTimeout after 3.0s
httpx2.get(url)                    ReadTimeout after 5.1s

The requests docs do tell you to always pass timeout=, and linters like Ruff’s S113 rule flag calls without one, yet plenty of code still ships without it. aiohttp’s default total=300 is technically a timeout, though five minutes is long enough that a stuck upstream will pile up waiting coroutines before anything fires.

Async results: aiohttp vs httpx vs httpx2

The async numbers spread far wider than the sync ones. Requests per second for 5,000 small GETs at each concurrency level (the concurrency-50 row comes from a separate re-run with 3 timed batches):

Concurrencyaiohttp 3.13.5httpx 0.28.1httpx2 2.16.0aiohttp vs httpx
104,7705187889.2x
504,93044362411.1x
1004,13735442211.7x
5003,74919610819.1x
100, 94 KB body3,3313473829.6x

CPU time tells the same story more bluntly. At concurrency 100, aiohttp spent 240 µs of client CPU per request; httpx spent 2,822 µs and httpx2 2,368 µs.

aiohttp barely notices concurrency. It held between 3,700 and 4,900 requests per second from 10 to 500 in-flight requests and spent roughly 200-270 µs of CPU on each one. Its response parser is compiled C (aiohttp._http_parser in this install), so per-request work stays cheap even when hundreds of responses arrive at once.

httpx’s AsyncClient gets slower as you add concurrency. At 10 concurrent requests it managed 518 req/s, which is less than half of what the synchronous httpx client did sequentially (1,130). At 500 it fell to 196, burning over 5 ms of CPU per request. The gap has been known for years: in httpx issue #838 back in 2020, a maintainer conceded httpx was about 2-3x slower than aiohttp and said “getting up to speed there isn’t a priority for 1.0.” Other public benchmarks land in the same range or worse: Oxylabs measured 10.22 seconds for httpx against 3.79 for aiohttp at 1,000 requests, and a DEV Community write-up that matched pool sizes reported about 6x. My gap is larger, probably because two cores make the CPU cost per request show up directly as lost throughput.

httpx2 beats httpx at the concurrency levels most code runs at, and loses at the extreme. At concurrency 10 it did 52% more requests than httpx, and 41% more at 50. The httpcore2 changelog lists the likely reasons: a single-pass rewrite of _assign_requests_to_connections, anyio’s fast_acquire for locks, and a change that assigns each released connection to exactly one queued request. At concurrency 500, though, httpx2 dropped to 108 req/s with a 38% spread between runs. I re-ran that case twice to make sure it wasn’t a fluke: httpx2 scored 110 and 154, httpx 210 and 194. The server-side connection counter showed one difference. httpx opened about 300 TCP connections over the run, httpx2 opened 500 to 625. I haven’t traced why, so treat it as an open question about httpx2 on a CPU-starved box. At that concurrency, aiohttp (or the transport swap in the next section) is the better tool for both.

Faster async httpx2: the aiohttp transport

Most of aiohttp’s speed is available without rewriting httpx code. httpx’s transport layer is pluggable, and the httpx-aiohttp package swaps the httpcore pool for an aiohttp session underneath the same API. Version 0.2.0 ships an httpx_aiohttp.httpx2 variant. This is the same arrangement the OpenAI README recommends: “By default, the async client uses HTTPX2. For improved concurrency performance, you may also use aiohttp as the HTTPX2 transport.”

import asyncio
import time

import httpx2
from httpx_aiohttp.httpx2 import Httpx2AiohttpClient

URL = "http://127.0.0.1:8765/small"


async def hammer(client, n=5000, conc=100):
    sem = asyncio.Semaphore(conc)

    async def one():
        async with sem:
            r = await client.get(URL)
            r.raise_for_status()

    t0 = time.perf_counter()
    await asyncio.gather(*(one() for _ in range(n)))
    return n / (time.perf_counter() - t0)


async def main():
    limits = httpx2.Limits(max_connections=100, max_keepalive_connections=100)
    async with httpx2.AsyncClient(limits=limits) as plain:
        print(f"httpx2, default transport: {await hammer(plain):6.0f} req/s")
    async with Httpx2AiohttpClient(limits=limits) as fast:  # same httpx2 API
        print(f"httpx2, aiohttp transport: {await hammer(fast):6.0f} req/s")
        print(type(fast._transport).__name__)


asyncio.run(main())

Against the same local server:

httpx2, default transport:    401 req/s
httpx2, aiohttp transport:   1904 req/s
AiohttpTransport

In the full harness (median of 3 batches of 5,000), the aiohttp transport gave httpx2 1,906 req/s at concurrency 10, 1,651 at 100, and 1,682 at 500, using 520-600 µs of CPU per request. It also stayed flat at 500, exactly where plain httpx2 fell over. Raw aiohttp was still 2.5-3x faster in the same runs, because each request still goes through httpx2’s Request/Response objects, but the 4.2x jump over the default transport costs you a one-line change.

With the OpenAI SDK, it looks like this. I didn’t execute it here because it needs an API key, so treat it as the README’s documented usage:

# pip install "openai[aiohttp]"   (not run here: requires OPENAI_API_KEY)
from openai import AsyncOpenAI, DefaultAioHttpClient

async with AsyncOpenAI(http_client=DefaultAioHttpClient()) as client:
    ...

If a service fans out hundreds of LLM calls at once (an eval run, a batch classifier), that’s the switch to flip before reaching for more workers. Wrap the calls in a TaskGroup instead of bare gather so one failed request cancels the rest instead of leaving orphans.

Migrating from httpx to httpx2

httpx2’s migration guide describes it as a fork of 0.28.1 with the same public API. The 2.0.0b1 changelog lists only the renames as breaking changes: the package (httpx to httpx2), the transport (httpcore to httpcore2), the CLI, the logger name, and the User-Agent header. For your own code, that’s a find-and-replace on imports and exception names.

The awkward case is a dependency you don’t control that still imports httpx. For applications (not libraries), httpx2 2.9.0 added alias_httpx() (changelog):

import httpx2

httpx2.alias_httpx()  # must run before anything imports httpx or httpcore

import httpx           # resolves to httpx2 from here on
import httpcore

print(httpx.__name__, httpx.__version__, "| httpcore ->", httpcore.__name__)
with httpx.Client() as client:
    print(client.headers["user-agent"])
httpx2 2.16.0 | httpcore -> httpcore2
python-httpx2/2.16.0

The docs list the catches. Call it at the very top of your entrypoint, or it raises RuntimeError. logging.getLogger("httpx") won’t see the new logger (it’s httpx2 now, so update any setLevel calls like the one in the tenacity retry setup). importlib.metadata.version("httpx") still reads the old package’s metadata, and subprocesses need to call it again.

Two behavior changes are worth checking before you deploy:

  1. The certificate source changed. Since 2.3.0, httpx2 uses truststore on macOS and Windows. On Linux, the httpcore2 source loads OpenSSL’s compiled-in CA paths (with fallbacks like /etc/ssl/certs/ca-certificates.crt). Corporate CAs installed system-wide start working without SSL_CERT_FILE hacks. The flip side, going by that same code path (I didn’t test it in a container): an image with no system CA bundle has nothing to load, so set SSL_CERT_FILE or install ca-certificates.
  2. Python 3.11+ only. httpx2 2.15.0 dropped 3.10 in October. If you’re stuck on 3.10, pin httpx2<2.15, or stay on httpx 0.28.1 for now.

Import cost also went down. Measured as the median of 15 fresh interpreters, minus a bare python -c pass (45 ms):

import requests  +231 ms
import httpx     +241 ms
import httpx2    +167 ms
import aiohttp   +380 ms

A second run gave +229, +263, +161 and +357 ms. httpx2 doesn’t import certifi and loads its CLI module lazily since 2.2.0, which plausibly accounts for most of the drop. For a CLI or a Lambda cold start, 70-100 ms on every invocation is noticeable, and lazy imports can hide the rest.

Feature comparison

requests 2.34httpx 0.28.1httpx2 2.16aiohttp 3.14
Sync APIYesYesYesNo
Async APINoYes (asyncio, trio)Yes (asyncio, trio)Yes (asyncio only)
HTTP/2NoOptional ([http2])Optional ([http2])No
WebSocketsNoVia httpx-wsBuilt in ([ws])Built in
Server-sent eventsNoVia httpx-sseBuilt in (client.sse())Manual
Default timeoutNone5 s5 s300 s total
Includes a serverNoNoNoYes (aiohttp.web)
Last stable releaseMay 2026Dec 2024Oct 2026Oct 2026
Role in the OpenAI and Anthropic SDKsNot usedDefault before openai 3.0 / anthropic 1.0Default since August 2026Opt-in [aiohttp] extra

The HTTP/2 row explains why httpx is still worth having. If you talk to a server that multiplexes many streams over one connection, aiohttp can’t do it. For most REST APIs behind a load balancer, HTTP/1.1 with keep-alive is usually good enough. aiohttp’s async-only design is the other real constraint: a codebase with both sync and async callers ends up with two clients and two sets of error handling.

Which one should you use?

  • A script, notebook or CLI that makes a handful of calls: requests or httpx2. Speed doesn’t matter at that scale, but httpx2 gives you the 5-second default timeout for free.
  • Sync service code: httpx2’s Client, which did 51% more requests per second than requests here and is actively maintained.
  • Async code with fewer than ~50 requests in flight: httpx2’s AsyncClient. It’s fast enough, and you keep one API across sync and async callers.
  • Async fan-out with hundreds of requests in flight: aiohttp, or httpx2 on the aiohttp transport. That’s 4x to 19x the throughput of the httpcore pool in these runs.
  • You need HTTP/2: httpx2 with the [http2] extra, since neither aiohttp nor requests speaks it.
  • Existing httpx 0.28 code you can’t touch yet: call httpx2.alias_httpx() at the entrypoint.
  • A library you publish on PyPI: depend on httpx2 directly, because alias_httpx() is for applications only.

My default for new code is httpx2: one API for sync and async, sane timeouts, and a maintainer that shipped 20 releases in its first five months. For anything that fans out, I’d put it on the aiohttp transport from day one, before latency becomes the problem. Plain aiohttp is still the right call when you want every last request per second and you’re fully async anyway, a crawler or a load generator for example. And if your service sits on FastAPI, the FastAPI vs Litestar comparison covers the server side of the same stack.

FAQ

Is httpx faster than aiohttp?

No. In these benchmarks aiohttp was 9.2x faster than httpx’s AsyncClient at 10 concurrent requests and 19.1x faster at 500, and it used about a tenth of the CPU per request. httpx’s own maintainers acknowledged the gap in 2020 (issue #838). httpx2 narrows it a little at moderate concurrency, and putting httpx2 on the aiohttp transport closes most of it.

Is httpx better than requests?

For new code, yes. httpx did 36% more requests per second than requests in the sequential test, it has a default 5-second timeout where requests has none, and it supports async and HTTP/2. In 2026, though, the httpx API is best used through httpx2, because httpx itself hasn’t had a stable release since December 2024.

Is httpx still maintained?

The encode/httpx repository isn’t archived, and new 1.0 development builds were uploaded to PyPI in August 2026, but the last stable release is 0.28.1 from December 2024 and the 1.0 redesign isn’t public yet. Pydantic’s httpx2 fork is where the bug fixes and security updates are happening, which is why the OpenAI and Anthropic SDKs switched to it.

What is httpx2?

httpx2 is Pydantic’s fork of httpx 0.28.1, first released on May 12, 2026. It keeps the httpx API under a new import name (import httpx2), renames httpcore to httpcore2, and has since added WebSockets, server-sent events, OS trust store support and connection-pool performance work. httpx2.alias_httpx() lets applications redirect import httpx to it.

Should I use aiohttp or httpx for FastAPI?

For outbound calls from a FastAPI app, either works, since both are asyncio-native. Use httpx2’s AsyncClient if you want the same client in tests and sync code, and switch to the aiohttp transport (or aiohttp itself) when one request handler fans out to many upstream calls. Create the client once at startup and reuse it; a new client per request throws away the connection pool.

Sources

Bottom line

aiohttp has out-run httpx on overlapping requests for years, and in these runs the margin was an order of magnitude once more than a handful of requests were in flight. What changed in 2026 is maintenance: the httpx API lives on as httpx2, the httpx package is frozen at 0.28.1, and the biggest Python SDKs have already moved. Switch your imports to httpx2, use the aiohttp transport wherever you fan out, and pass timeout= to every requests call that’s still in your codebase.