httpx 是 requests 的现代替代品,主打两件事:原生支持 async/await,以及 HTTP/2。社区里常见的说法是「并发场景换 httpx,性能翻几倍」。按这个说法在 Windows 上跑了一轮,结论和预期不一样。
先看它在低并发下的表现
测试方法:本地起一个 HTTP 服务,每次请求固定延迟 50 毫秒(模拟真实网络往返),然后用四种方式各打 N 个请求。
异步:asyncio.gather 同时发 N 个
同步:requests 线程池 N 个 worker
串行:for 循环一次一个
先看 50 并发:
| 方式 | 50 个请求耗时 | 相对串行 |
|---|---|---|
| httpx 异步(默认连接池) | 1.00 秒 | — |
| httpx 异步(放大连接池到 60) | 0.89 秒 | — |
| requests 线程池 50 worker | 1.08 秒 | — |
这个量级下三者差不多,httpx 放大连接池后略快 10%。50 个请求各 50 毫秒,理论上限是 50 毫秒(全部并行),实际要 0.89 秒,说明还有别的开销压着。
200 并发时它反而退化了
把并发提到 200,结果反转:
| 方式 | 200 个请求耗时 | 变化 |
|---|---|---|
| httpx 异步(默认连接池) | 2.94 秒 | 比 50 并发慢 2.9 倍 |
| httpx 异步(放大到 210) | 3.03 秒 | 放大反而更慢 |
| requests 线程池 200 worker | 1.17 秒 | 基本没变 |
200 个请求、每个 50 毫秒,理想情况应该 50 毫秒左右完成。requests 线程池跑到 1.17 秒(已经不算好,但线性没退化);httpx 反而涨到 2.94 秒,比线程池慢 2.5 倍。
把连接池上限从默认值放大到 210 也没救,3.03 秒,甚至更慢。说明瓶颈不在连接数限制,而在别处。
为什么会在 Windows 上退化
测试环境是 Windows 10,asyncio 用的是 WindowsProactorEventLoopPolicy。这个事件循环底层是 IOCP,单个连接的开销比 Linux 的 epoll 高,在几百个并发连接同时建立时尤其明显。httpx 每个请求都要走一遍 asyncio 的协程调度,200 个协程的调度成本叠起来就变成了秒级。
requests 的线程池是另一种模型:200 个 OS 线程,每个线程阻塞在自己的 socket 上。线程切换有成本,但 Windows 的线程调度对这种情况优化得不错,所以反而稳定。
另外服务端也有影响。测试用的是 ThreadingTCPServer,每个连接一个线程,200 个连接就是 200 个服务端线程,这部分开销两边都要承担。
那 httpx 的优势在哪
不在「本地高并发」。它真正的价值是这几件事:
接口和 requests 几乎一样(client.get(url, params=...)),从 requests 迁过来改几行就行,不像 aiohttp 那样要重写整个调用链。
同一个库既能同步又能异步。写脚本时用 Client,写服务时换 AsyncClient,代码结构不用变,这在混合场景里很省事。
原生支持 HTTP/2。多路复用能在一条 TCP 连接上并发多个请求,这个优势在对同一个域发大量请求时明显,但本次测的是 HTTP/1.1 的本地回环,这块没覆盖到。
超时、重试、连接池的配置比 requests 更细,比如 httpx.Timeout(connect=2, read=5) 能分别控制连接和读取的超时,requests 只有一个总的。
什么时候别用它
如果你只是想在 Windows 上并发抓几百个 URL,线程池 + requests 更省心,实测就是快一倍多。为了 async 而 async,在 Windows 上不一定划算。
部署在 Linux 上结论可能不一样(epoll 比 IOCP 高效得多),但 Linux 上的表现没测,不能直接套用这个结论。
还有一点:AsyncClient 必须复用,不能每次请求都新建。每次新建会重新建立连接池,实测这种写法比串行还慢。正确写法是 async with httpx.AsyncClient() as c: 包住整个循环。
没测的部分列一下,3 项:Linux 上的同批对比 / HTTP/2 多路复用的实际收益 / 与 aiohttp 的横向对比。








