skip to content

httpx β€” Modern HTTP Client

Make sync and async HTTP requests with httpx. Covers GET/POST, async usage, HTTP/2, streaming, and how it compares to requests.

2 min read 8 snippets yesterday intermediate

httpx β€” Modern HTTP Client#

What it is#

httpx is a fully-featured HTTP client that supports both synchronous and asynchronous usage. It’s a drop-in replacement for requests in synchronous code and the go-to choice when you need async, HTTP/2, or type-annotated APIs.

Install#

pip install httpx
# HTTP/2 support (optional)
pip install "httpx[http2]"

Quick example β€” synchronous#

import httpx

resp = httpx.get("https://httpbin.org/get", params={"tool": "httpx"})
resp.raise_for_status()
print(resp.status_code)
print(resp.json()["args"])

Output:

200
{'tool': 'httpx'}

Async example#

import asyncio
import httpx

async def fetch_all(urls: list[str]) -> list[dict]:
    async with httpx.AsyncClient(timeout=10) as client:
        tasks = [client.get(url) for url in urls]
        responses = await asyncio.gather(*tasks)
        return [r.json() for r in responses]

urls = [
    "https://httpbin.org/get?n=1",
    "https://httpbin.org/get?n=2",
    "https://httpbin.org/get?n=3",
]
results = asyncio.run(fetch_all(urls))
print([r["args"] for r in results])

Output:

[{'n': '1'}, {'n': '2'}, {'n': '3'}]

When / why to use it over requests#

SituationUse
You need async/awaithttpx
You want HTTP/2httpx
Type annotations matter to youhttpx (fully typed)
Simple sync-only scriptsEither (requests has wider ecosystem)
Existing requests codebaseKeep requests unless you have a reason to migrate

Common pitfalls#

[!WARNING] AsyncClient must be used as a context manager β€” creating httpx.AsyncClient() without async with leaks the connection pool. Always use async with httpx.AsyncClient() as client:.

[!WARNING] Default timeout is 5 seconds β€” unlike requests, httpx has a default timeout. You can increase or disable it: timeout=httpx.Timeout(30.0) or timeout=None.

[!TIP] httpx raises httpx.HTTPStatusError (not requests.HTTPError) when you call raise_for_status(). Handle imports accordingly if porting from requests.

Richer example β€” POST with auth and retry#

import httpx
from tenacity import retry, stop_after_attempt, wait_exponential

@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=0.5))
def post_event(payload: dict) -> dict:
    with httpx.Client(timeout=10) as client:
        resp = client.post(
            "https://httpbin.org/post",
            json=payload,
            headers={"Authorization": "Bearer my-token"},
        )
        resp.raise_for_status()
        return resp.json()["json"]

result = post_event({"event": "page_view", "user": "alice"})
print(result)

Output:

{'event': 'page_view', 'user': 'alice'}

[!NOTE] tenacity is a separate retry library (pip install tenacity). httpx does not include built-in retry logic the way requests + HTTPAdapter does.

Quick reference#

# Sync client (recommended: reuse across calls)
with httpx.Client(base_url="https://api.example.com", timeout=10) as client:
    r = client.get("/users", params={"page": 1})
    r = client.post("/users", json={"name": "Alice"})
    r = client.put("/users/1", json={"name": "Alice B."})
    r = client.delete("/users/1")

# One-off sync calls
httpx.get(url)
httpx.post(url, json={...})

# Async
async with httpx.AsyncClient() as client:
    r = await client.get(url)