Why I Ditched Requests for Httpx: A Guide to Async HTTP Client and HTTP/2 for Python

Python tutorial - IT technology blog
Python tutorial - IT technology blog

When Requests is No Longer Fast Enough for Modern Needs

In the Python world, requests is like an uncrowned king. It is simple, stable, and extremely popular. However, when I started deploying monitoring alert systems and production deployment scripts running on asyncio, requests suddenly became a bottleneck that slowed down the entire system.

The problem lies in its synchronous (blocking) mechanism. Imagine your script needs to call 50 APIs, each taking 1 second to respond. With requests, it takes exactly 50 seconds because the program must wait for each request one by one. In low-latency applications like FastAPI or Telegram Bots, letting the CPU “sit idle” waiting for the network is a terrible waste. That’s when I turned to httpx.

I have migrated all my automation tools to httpx for over six months now. The results are surprising. It not only solves the async/await problem but also brings features that requests still lacks: HTTP/2 and a true Connection Pooling mechanism.

What Makes Httpx the New Hype Among Python Devs?

Simply put, httpx is like an “engine upgrade” for requests. It maintains the familiar API to help you switch your code in a heartbeat, but underneath is a completely different processing system.

Key Advantages:

  • Full Async Support: Perfect compatibility with asyncio and trio.
  • Easy Migration: You almost only need to change import requests to import httpx.
  • HTTP/2 Multiplexing: Send multiple requests over a single TCP connection. This reduces latency from 200ms to about 40-50ms for subsequent requests.
  • Connection Pool Management: Smart connection reuse, eliminating time-consuming SSL/TLS handshakes.
  • Strict Timeouts: Prevents scripts from hanging indefinitely thanks to strict default timeout configurations.

Installation and Configuration

You can install the standard version or the full version with HTTP/2 support via pip. I recommend the full version to take full advantage of its power.

pip install httpx[http2]

Sync Mode: Familiar and Straightforward

If you don’t need async yet, httpx still serves traditional tasks well with a syntax identical to its predecessor:

import httpx

# Syntax identical to requests
resp = httpx.get("https://api.github.com/repos/encode/httpx")
print(f"Status: {resp.status_code} - Repo: {resp.json()['name']}")

Async Mode: Breaking Speed Limits

This is the real reason to switch to httpx. When you need to crawl data from 100 websites or send 50 simultaneous notifications, async will save you dozens of precious seconds.

import httpx
import asyncio

async def fetch_data():
    async with httpx.AsyncClient() as client:
        resp = await client.get("https://www.google.com")
        print(f"Done: {resp.status_code}")

asyncio.run(fetch_data())

Note: Always use AsyncClient within an async with block. This habit ensures connections are automatically closed, preventing resource leaks during long-term system operation.

Optimizing Performance with Connection Pooling

Many developers make the mistake of initializing a new Client for every request. This slows down the script because it has to repeat the time-consuming TCP/SSL handshake process. Connection Pooling was created to end this waste.

Keep a single Client instance for the entire lifecycle of your application:

import httpx
import asyncio
import time

async def main():
    # Initialize once, use for thousands of requests
    async with httpx.AsyncClient() as client:
        urls = ["https://httpbin.org/get"] * 50
        start = time.perf_counter()
        
        tasks = [client.get(url) for url in urls]
        results = await asyncio.gather(*tasks)
        
        duration = time.perf_counter() - start
        print(f"Processed {len(results)} requests in {duration:.2f}s")

asyncio.run(main())

In production, using pooling has helped me reduce total execution time by up to 40% for APIs requiring strict HTTPS.

Boosting Speed with HTTP/2

HTTP/1.1 is quite dated as each request usually occupies its own connection. In contrast, HTTP/2 allows multiple requests to be pushed simultaneously through a single “pipe.” To activate it, you just need to add a simple parameter.

async with httpx.AsyncClient(http2=True) as client:
    resp = await client.get("https://www.google.com")
    print(f"Version: {resp.http_version}") # Output: HTTP/2

If the destination server does not support HTTP/2, httpx will gracefully fallback to HTTP/1.1 without causing program errors.

Error Handling and Timeouts in Production

By default, httpx sets a timeout of 5 seconds. This can sometimes be too short for APIs processing heavy data. I usually configure it in detail for a more resilient script:

# Fast connection (2s), but allow more time for reading data
timeout = httpx.Timeout(10.0, connect=2.0, read=None)

try:
    async with httpx.AsyncClient(timeout=timeout) as client:
        resp = await client.get("https://slow-api.com")
        resp.raise_for_status() 
except httpx.ConnectTimeout:
    print("Server did not respond during the handshake!")
except httpx.HTTPStatusError as exc:
    print(f"Error {exc.response.status_code} at {exc.request.url}")
except httpx.RequestError:
    print("Unknown network error")

Conclusion from Real-World Experience

Switching to httpx isn’t just about chasing new technology. It’s an upgrade in mindset when you start building high-performance Python systems.

For small, one-off scripts, requests is still a good companion. But if you are working with FastAPI, building high-speed crawlers, or managing thousands of requests per minute, httpx is an irreplaceable choice. Its clear debugging capabilities will save you many times when operating in production.

Share: