Compare
httpx vs requests vs aiohttp: choosing an HTTP client for crawling
Compare sync and async Python HTTP clients on API, HTTP/2, connection pooling, timeouts, and concurrency. No invented benchmarks; documented behaviour and practical guidance.
requests is the default synchronous Python HTTP client. httpx is a modern client with sync and async APIs and HTTP/2 support. aiohttp is an async client/server framework built on asyncio. All three can fetch a page; they differ in concurrency model, API, and ecosystem.
At a glance
| Criterion | requests | httpx | aiohttp |
|---|---|---|---|
| Sync API | Yes | Yes | No (async only) |
| Async API | No | Yes | Yes |
| HTTP/2 | No (urllib3/1.1) | Yes (optional h2) |
No (HTTP/1.1) |
| Connection pooling | Session |
Client |
ClientSession |
| Timeouts | timeout tuple |
Timeout object |
ClientTimeout |
| Redirects | allow_redirects |
follow_redirects |
allow_redirects |
| Cookie jar | Yes | Yes | Yes |
| Proxy support | Yes | Yes | Yes |
| Streaming | Yes | Yes | Yes |
| Ecosystem | Huge | Growing | Large in asyncio |
| Learning curve | Low | Low | Medium |
When to use requests
Use requests when:
- the crawl is small or medium and synchronous is enough;
- you want the largest ecosystem of examples and adapters;
- you need a simple
Sessionwith cookies and retries; - you are writing a script, not a high-concurrency service.
It is the easiest client to debug and the one most tutorials assume. For a polite crawl at one request per second, the concurrency model is irrelevant.
When to use httpx
Use httpx when:
- you want one API for sync and async code;
- you need HTTP/2;
- you want a modern
Clientwith explicit timeouts and connection limits; - you are already using an async framework and do not want a second client.
import httpx
with httpx.Client(
timeout=httpx.Timeout(10.0, connect=5.0),
follow_redirects=True,
headers={"User-Agent": "CrawlerBros-Example/1.0"},
) as client:
response = client.get("https://example.com/")
print(response.status_code, response.http_version)
This is illustrative. The important part is the explicit timeout object and the client reuse; creating a new client per request throws away the connection pool.
When to use aiohttp
Use aiohttp when:
- you are building an asyncio service and need a mature async client;
- you need a client and server in the same framework;
- you want fine-grained control over connectors and concurrency.
import asyncio
import aiohttp
async def main():
timeout = aiohttp.ClientTimeout(total=10)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get("https://example.com/") as response:
print(response.status, response.content_type)
asyncio.run(main())
Again illustrative. aiohttp is fast and capable, but the async model infects the rest of your code. Do not choose it just to “go faster” on a crawl that is limited by politeness.
Concurrency is not the same as speed
A synchronous client with one worker sends one request at a time. An async client can have many requests in flight, but if the site allows one request per second, the async client must still wait. Concurrency helps when latency dominates and the target permits parallelism; it does not override a rate limit.
Use a per-host limiter with any client. The rate limiting guide shows the pattern.
Timeouts and retries
- requests:
timeout=(connect, read); retries viaHTTPAdapter/urllib3 or a loop. - httpx:
httpx.Timeout(connect=..., read=..., write=..., pool=...); transport retries are available. - aiohttp:
ClientTimeout(total=..., connect=...); retries are usually a loop or a middleware.
Set a connect timeout and a read timeout separately. A connect timeout usually means the host is unreachable; a read timeout may mean a slow server. Do not set one infinite timeout and call it a day.
A practical recommendation
- Scripts and small crawls:
requests. - New sync or async Python code:
httpx. - Existing asyncio service:
aiohttp. - High-concurrency crawl: async client plus a per-host limiter, not just a bigger thread pool.
The HTTP client is rarely the reason a crawl fails. The failure is usually the target’s terms, a missing robots check, a rate limit, or a parser that broke. Choose the client that makes those behaviours easy to implement and test.