Comparing requests and httpx in Python: Which HTTP Client Should You Use in 2025?
by Gary Worthington, More Than Monkeys

If you’ve been writing Python code that talks to APIs, chances are you’ve used the requests library. It’s the de facto standard, and for good reason -it’s simple, stable, and well-documented. However in recent years, httpx has emerged as a modern alternative that adds HTTP/2, connection pooling, timeouts, andprobably most interestingly, async support.
So should you stick with requests, or is it time to switch to httpx?
In this post, I’ll compare the two libraries side by side, taking a look at:
- API design and ergonomics
- Synchronous and asynchronous requests
- Timeouts and retries
- Testing and mocking
- SSL and mutual TLS (mTLS)
- Connection pooling
- Performance benchmarks
- When to use which
1. Quick Comparison
requests:
- Simple, synchronous
- Mature and widely used
- No support for async or HTTP/2
httpx:
- Drop-in compatible with requests
- Supports async and HTTP/2
- Better connection pooling and testing support
Let’s look at what this means in practice.
2. Making Simple Requests
Here’s the basic usage of both libraries.
Using requests:
import requests
def fetch_user(user_id):
response = requests.get(f"https://api.example.com/users/{user_id}")
response.raise_for_status()
return response.json()
Using httpx:
import httpx
def fetch_user(user_id):
with httpx.Client() as client:
response = client.get(f"https://api.example.com/users/{user_id}")
response.raise_for_status()
return response.json()
For basic synchronous use, they’re nearly identical.
3. Async Support in httpx
This is where httpx stands apart. If your app is async (think FastAPI, asyncio workers etc) requests won’t help you.
import httpx
import asyncio
async def fetch_user_async(user_id):
async with httpx.AsyncClient() as client:
response = await client.get(f"https://api.example.com/users/{user_id}")
response.raise_for_status()
return response.json()
No async support exists in requests, nor is itplanned.
4. Timeouts and Retries
With requests:
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
session = requests.Session()
retry = Retry(total=3, backoff_factor=0.3)
adapter = HTTPAdapter(max_retries=retry)
session.mount("https://", adapter)
response = session.get("https://api.example.com/data", timeout=5)
With httpx:
import httpx
client = httpx.Client(
timeout=httpx.Timeout(5.0),
limits=httpx.Limits(max_connections=10)
)
response = client.get("https://api.example.com/data")
Retries in httpx aren’t built-in (yet), but there are third-party libraries like resilient-httpx to help.
5. Testing and Mocking
Testing HTTP logic with requests often involves external libraries like responses or requests-mock.
Example with requests:
import responses
from my_app import fetch_user
@responses.activate
def test_fetch_user():
responses.add(
responses.GET,
"https://api.example.com/users/123",
json={"id": 123, "name": "Alice"},
status=200
)
user = fetch_user(123)
assert user["name"] == "Alice"
Example with httpx:
import httpx
from my_app import fetch_user
def test_fetch_user():
def handler(request):
assert request.url.path == "/users/123"
return httpx.Response(200, json={"id": 123, "name": "Alice"})
transport = httpx.MockTransport(handler)
client = httpx.Client(transport=transport)
user = fetch_user(123, client=client)
assert user["name"] == "Alice"
You get built-in mocking with httpx, and it works for async and sync clients alike.
6. File Uploads
Both libraries support file uploads the same way:
files = {'file': open('report.csv', 'rb')}
requests.post("https://api.example.com/upload", files=files)
httpx.post("https://api.example.com/upload", files=files)
No surprises here.
7. SSL/TLS Support
Both libraries validate HTTPS by default and support custom CA bundles and client certificates.
With requests:
requests.get("https://example.com", verify="/path/to/ca.pem")
requests.get("https://example.com", cert="/path/to/client.pem")
You can also disable verification (not recommended):
requests.get("https://example.com", verify=False)
With httpx:
httpx.get("https://example.com", verify="/path/to/ca.pem")
httpx.get("https://example.com", cert="/path/to/client.pem")
The syntax is nearly identical, but httpx allows more flexibility when combining options like HTTP/2, connection limits, and async.
8. Mutual TLS (mTLS)
Mutual TLS is used when both client and server need to verify each other’s identity. This is common in microservices, internal APIs, or regulated industries.
In requests:
requests.get(
"https://secure.example.com",
cert=("/path/to/cert.pem", "/path/to/key.pem"),
verify="/path/to/ca.pem"
)
In httpx:
client = httpx.Client(
cert=("/path/to/cert.pem", "/path/to/key.pem"),
verify="/path/to/ca.pem"
)
response = client.get("https://secure.example.com")
Async support makes httpx a better choice for internal service-to-service APIs secured by mTLS.
9. Connection Pooling
By default, each call to requests.get(...) opens a new connection unless you use a Session. With httpx, persistent connection pooling is the default with Client and AsyncClient.
Using requests with a session:
session = requests.Session()
session.get("https://api.example.com")
Using httpx with connection limits:
client = httpx.Client(
limits=httpx.Limits(
max_connections=10,
max_keepalive_connections=5
)
)
This saves time, reduces socket overhead, and enables HTTP/2 multiplexing.
10. Performance Benchmarks
Here’s a benchmark comparing sync vs async clients making 100 requests.
import time
import asyncio
import requests
import httpx
URL = "https://httpbin.org/get"
N = 100
def benchmark_requests():
start = time.perf_counter()
for _ in range(N):
requests.get(URL)
return time.perf_counter() - start
def benchmark_httpx_sync():
client = httpx.Client()
start = time.perf_counter()
for _ in range(N):
client.get(URL)
return time.perf_counter() - start
async def benchmark_httpx_async():
async with httpx.AsyncClient() as client:
start = time.perf_counter()
tasks = [client.get(URL) for _ in range(N)]
await asyncio.gather(*tasks)
return time.perf_counter() - start
print(f"[requests] {benchmark_requests():.2f}s")
print(f"[httpx sync] {benchmark_httpx_sync():.2f}s")
print(f"[httpx async] {asyncio.run(benchmark_httpx_async()):.2f}s")
Typical results:
[requests] 12.0s
[httpx sync] 12.4s
[httpx async] 1.5s
11. Performance: Pooling Under Load
Let’s test how pooling affects performance:
import time
import httpx
URL = "https://httpbin.org/get"
N = 100
def no_pooling():
start = time.perf_counter()
for _ in range(N):
with httpx.Client() as client:
client.get(URL)
return time.perf_counter() - start
def default_pooling():
client = httpx.Client()
start = time.perf_counter()
for _ in range(N):
client.get(URL)
return time.perf_counter() - start
def tuned_pooling():
client = httpx.Client(
limits=httpx.Limits(max_connections=20, max_keepalive_connections=10)
)
start = time.perf_counter()
for _ in range(N):
client.get(URL)
return time.perf_counter() - start
print(f"[No Pooling] {no_pooling():.2f}s")
print(f"[Default Pooling] {default_pooling():.2f}s")
print(f"[Tuned Pooling] {tuned_pooling():.2f}s")
Expect:
[No Pooling] 13.8s
[Default Pooling] 8.5s
[Tuned Pooling] 6.3s
The gains are even greater with AsyncClient and concurrent workloads.
12. Summary
Use requests if:
- Your project is synchronous
- You want minimal dependencies
- You don’t need HTTP/2 or async support
Use httpx if:
- You’re building async apps or workers
- You care about performance, pooling, or mTLS
- You want better testing and mocking support
- You need HTTP/2 or modern TLS flexibility
Final Thoughts
requests is still rock-solid. But if you’re working with modern Python, and especially async code, httpx is a smarter choice.
Try it on one service. Replace a slow polling script. Start small. Once you see the benefits, it’s hard to go back.
Gary Worthington is a software engineer, delivery consultant, and agile coach who helps teams move fast, learn faster, and scale when it matters. He writes about modern engineering, product thinking, and helping teams ship things that matter.
Through his consultancy, More Than Monkeys, Gary helps startups and scaleups improve how they build software — from tech strategy and agile delivery to product validation and team development.
Visit morethanmonkeys.co.uk to learn how we can help you build better, faster.
Follow Gary on LinkedIn for practical insights into engineering leadership, agile delivery, and team performance
A message from our Founder
Hey, Sunil here. I wanted to take a moment to thank you for reading until the end and for being a part of this community.
Did you know that our team run these publications as a volunteer effort to over 200k supporters? We do not get paid by Medium!
If you want to show some love, please take a moment to follow me on LinkedIn, TikTok and Instagram. And before you go, don’t forget to clap and follow the writer️!
Comparing requests and httpx in Python: Which HTTP Client Should You Use in 2025? was originally published in Python in Plain English on Medium, where people are continuing the conversation by highlighting and responding to this story.