Back to Blog
Python

python httpx vs requests vs aiohttp: Which to Use?

Compare python httpx vs requests vs aiohttp for sync/async HTTP, streaming, HTTP/2, and concurrency to pick the right client.

HTTP clientsAsync programminghttpxrequestsaiohttp
Diagram comparing Python HTTP clients httpx, requests, and aiohttp, highlighting sync and async request flows.

When you need to make HTTP requests in Python, the choice often comes down to three libraries: requests, httpx, and aiohttp. Each has a different design philosophy, and the right one depends on whether you need synchronous code, asynchronous concurrency, HTTP/2 support, or streaming. This article compares python httpx vs requests vs aiohttp across the dimensions that matter most in production.

The Core Difference: Sync vs Async

The most significant distinction is how each library handles blocking I/O. requests is strictly synchronous. Every call blocks the current thread until the response is received. aiohttp is built around asyncio and requires an event loop. httpx offers both worlds: it has a synchronous API similar to requests and an asynchronous API that mirrors aiohttp's style.

This affects not only the syntax but also how you structure your application. A synchronous client is simpler to reason about and works well in scripts, CLI tools, or low-concurrency services. An asynchronous client allows many requests to overlap without creating threads, which is essential for high-concurrency workloads like web scrapers, API gateways, or microservices that call many downstream endpoints.

Simple Request Comparison

Here is a minimal GET request in each library.

requests

import requests response = requests.get("https://api.example.com/data") print(response.status_code) print(response.json())

httpx (sync)

import httpx response = httpx.get("https://api.example.com/data") print(response.status_code) print(response.json())

The synchronous httpx API is intentionally close to requests. In fact, many codebases can switch by replacing import requests with import httpx and changing the call to httpx.get.

httpx (async)

import httpx import asyncio async def fetch(): async with httpx.AsyncClient() as client: response = await client.get("https://api.example.com/data") print(response.status_code) print(response.json()) asyncio.run(fetch())

aiohttp

import aiohttp import asyncio async def fetch(): async with aiohttp.ClientSession() as session: async with session.get("https://api.example.com/data") as response: print(response.status) print(await response.json()) asyncio.run(fetch())

Notice that aiohttp requires you to use a ClientSession and an async context manager for both the session and the response. httpx.AsyncClient is similar but slightly more concise. The requests library has no async mode at all.

Timeouts and Retries

Timeouts are critical for production reliability. All three libraries let you set connection and read timeouts, but the syntax differs.

requests

response = requests.get("https://api.example.com", timeout=(3.05, 10))

The tuple means (connect_timeout, read_timeout). A single float applies to both.

httpx

with httpx.Client(timeout=10.0) as client: response = client.get("https://api.example.com")

httpx uses a Timeout object that can be configured per request or per client. You can also set different values for connect, read, write, and pool timeouts.

aiohttp

timeout = aiohttp.ClientTimeout(total=10, connect=5) async with aiohttp.ClientSession(timeout=timeout) as session: async with session.get("https://api.example.com") as response: pass

Retries are not built into any of these libraries. You typically implement them with a loop or a library like tenacity. The key difference is that in async code, you must await between attempts to avoid blocking the event loop.

Streaming Responses

Streaming matters when you download large files or want to process a response incrementally.

requests

with requests.get("https://api.example.com/large-file", stream=True) as response: for chunk in response.iter_content(chunk_size=8192): process(chunk)

httpx

with httpx.stream("GET", "https://api.example.com/large-file") as response: for chunk in response.iter_bytes(): process(chunk)

aiohttp

async with session.get("https://api.example.com/large-file") as response: async for chunk in response.content.iter_chunked(8192): process(chunk)

All three support streaming, but the async versions require an async iterator. If you need to stream in a sync context, httpx and requests are the only options.

Concurrency and Performance

Concurrency is where the libraries diverge most. requests is blocking, so concurrent requests require threads or processes. Threads work for I/O-bound tasks but add overhead and complexity. aiohttp and httpx.AsyncClient use asyncio to multiplex requests on a single thread, which scales better when you have hundreds or thousands of concurrent connections.

A common pattern with httpx is to use asyncio.gather:

import asyncio import httpx async def fetch_url(client, url): response = await client.get(url) return response.json() async def main(): async with httpx.AsyncClient() as client: results = await asyncio.gather( fetch_url(client, "https://api.example.com/1"), fetch_url(client, "https://api.example.com/2"), fetch_url(client, "https://api.example.com/3"), ) print(results) asyncio.run(main())

The same pattern works with aiohttp using ClientSession. The performance difference between httpx and aiohttp is generally small; both are I/O-bound and rely on the event loop. The real performance gap is between blocking and non-blocking I/O, not between the two async libraries.

HTTP/2 and Advanced Features

HTTP/2 support is a differentiator. requests does not support HTTP/2. aiohttp also lacks native HTTP/2 support. httpx supports HTTP/2 when the optional h2 package is installed, and you enable it by passing http2=True to the client.

import httpx with httpx.Client(http2=True) as client: response = client.get("https://api.example.com")

HTTP/2 allows multiplexing multiple requests over a single connection, which can reduce latency in high-throughput scenarios. If you need this feature, httpx is the only choice among the three.

Here is a feature comparison table:

Featurerequestshttpxaiohttp
Sync APIYesYesNo
Async APINoYesYes
HTTP/2NoOptionalNo
StreamingYesYesYes
Connection poolingYesYesYes
Built-in retriesNoNoNo
WSGI/ASGI integrationNoYesYes

Error Handling

Each library raises its own exception types. requests raises requests.exceptions.RequestException and subclasses like Timeout and ConnectionError. httpx has httpx.RequestError and similar subclasses. aiohttp uses aiohttp.ClientError.

When writing code that might switch libraries, it's common to catch a broad exception and log the type. But for production, you should catch specific exceptions to avoid hiding programming errors.

import httpx try: response = httpx.get("https://api.example.com" except httpx.TimeoutException: print("Request timed out") except httpx.NetworkError: print("Network error")

Choosing the Right Library

The decision comes down to your project's constraints:

  • Use requests when you need a stable, battle-tested sync client and don't need async or HTTP/2. It's the default choice for many scripts and small services.
  • Use aiohttp when you're already building an asyncio application and need a mature async client with a large ecosystem. It's also a solid choice for building async servers, since aiohttp includes a web framework.
  • Use httpx when you want a modern client that supports both sync and async, HTTP/2, and a consistent API. It's particularly good for new projects that may need to evolve from sync to async later.

If you're starting a new project and don't have a strong reason to use aiohttp, httpx is often the most flexible choice. It lets you write sync code initially and migrate to async without changing libraries.

Migration Considerations

Moving from requests to httpx is straightforward for sync code. Most calls map directly: requests.get becomes httpx.get, and the response object has similar attributes. The main differences are in timeout configuration and the client context manager.

Moving from aiohttp to httpx.AsyncClient is also manageable. The async context managers are similar, but httpx uses a single client object rather than separate session and response contexts. You'll need to adjust exception handling and response attribute names.

When migrating, pay attention to how each library handles connection pooling. requests uses Session objects, httpx uses Client or AsyncClient, and aiohttp uses ClientSession. Reusing these objects across requests is important for performance because it reuses TCP connections and TLS sessions.

python httpx vs requests vs aiohttp: Which HTTP Client? | RYUSLOG DEV