httpx cheatsheet. The modern requests + aiohttp replacement.
Install
uv add httpx
Quick
import httpx
r = httpx.get("https://api.example.com/users")
r.status_code # 200
r.json() # dict
r.text # str
r.content # bytes
r.headers # dict-like
r.raise_for_status() # raise on 4xx/5xx
Async
import httpx
async with httpx.AsyncClient() as client:
r = await client.get("https://api.example.com/users")
print(r.json())
Client (reusable, with config)
client = httpx.Client(
base_url="https://api.example.com",
timeout=10.0,
headers={"Authorization": "Bearer ..."},
follow_redirects=True,
)
r = client.get("/users") # uses base_url
r = client.post("/users", json={"name": "x"})
client.close()
# Or:
with httpx.Client(...) as client:
r = client.get(...)
AsyncClient with lifespan (FastAPI)
@asynccontextmanager
async def lifespan(app):
app.state.http = httpx.AsyncClient(timeout=10.0)
yield
await app.state.http.aclose()
# In handler
@app.get("/proxy")
async def proxy(request: Request):
r = await request.app.state.http.get("https://api.example.com/data")
return r.json()
Request methods
r = client.get(url, params={"q": "foo"})
r = client.post(url, json={"x": 1})
r = client.post(url, data={"x": 1}) # form
r = client.post(url, content=b"raw bytes")
r = client.put(url, json={...})
r = client.patch(url, json={...})
r = client.delete(url)
r = client.head(url)
r = client.options(url)
Headers / cookies
client.get(url, headers={"X-Custom": "v"})
client.get(url, cookies={"session": "..."})
# Persistent cookies
client = httpx.Client()
client.get("https://example.com/login", params={"u": "x"})
# Cookies stored; sent on next request automatically
Auth
# Basic
client.get(url, auth=("user", "pass"))
# Bearer
client.get(url, headers={"Authorization": "Bearer abc"})
# Custom auth class
class MyAuth(httpx.Auth):
def auth_flow(self, request):
request.headers["X-API-Key"] = self.key
yield request
client.get(url, auth=MyAuth())
Timeouts
# Total
httpx.get(url, timeout=10.0)
# Granular
httpx.get(url, timeout=httpx.Timeout(connect=5.0, read=10.0, write=5.0, pool=5.0))
# Disable
httpx.get(url, timeout=None)
Default: 5 seconds. Don’t disable — always have some limit.
Retries / transport
transport = httpx.HTTPTransport(retries=3)
client = httpx.Client(transport=transport)
For async:
transport = httpx.AsyncHTTPTransport(retries=3)
client = httpx.AsyncClient(transport=transport)
Built-in retries cover connection errors only; for HTTP-level retries, use tenacity.
tenacity retries
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
@retry(
retry=retry_if_exception_type((httpx.HTTPError, httpx.RequestError)),
stop=stop_after_attempt(5),
wait=wait_exponential(min=1, max=10),
)
async def fetch_with_retry(url: str) -> dict:
async with httpx.AsyncClient() as c:
r = await c.get(url, timeout=10)
r.raise_for_status()
return r.json()
Streaming
with httpx.stream("GET", url) as r:
for chunk in r.iter_bytes(chunk_size=8192):
process(chunk)
# Lines
with httpx.stream("GET", url) as r:
for line in r.iter_lines():
...
# JSON SSE
with httpx.stream("GET", url) as r:
for line in r.iter_lines():
if line.startswith("data:"):
data = json.loads(line[5:].strip())
yield data
Async:
async with httpx.AsyncClient() as c:
async with c.stream("GET", url) as r:
async for chunk in r.aiter_bytes():
...
File upload
files = {"file": open("data.csv", "rb")}
r = client.post(url, files=files)
# Multipart with additional fields
data = {"name": "alice"}
r = client.post(url, files=files, data=data)
Proxy
client = httpx.Client(proxies="http://proxy:8080")
# Or per-scheme
client = httpx.Client(proxies={
"http://": "http://proxy:8080",
"https://": "http://proxy:8080",
})
Verify TLS
httpx.get(url, verify="/path/to/ca.pem")
httpx.get(url, verify=False) # disable; testing only!
HTTP/2
client = httpx.Client(http2=True)
Requires h2 package: uv add httpx[http2].
Concurrent requests
async with httpx.AsyncClient() as c:
tasks = [c.get(url) for url in urls]
responses = await asyncio.gather(*tasks)
With concurrency limit:
sem = asyncio.Semaphore(10)
async def fetch(url):
async with sem:
return await c.get(url)
Errors
try:
r = client.get(url, timeout=5)
r.raise_for_status()
except httpx.TimeoutException: ...
except httpx.HTTPStatusError as e: ... # 4xx/5xx
except httpx.RequestError as e: ... # network errors
Events / hooks
def log_request(request):
print(f"-> {request.method} {request.url}")
def log_response(response):
response.read()
print(f"<- {response.status_code}")
client = httpx.Client(event_hooks={
"request": [log_request],
"response": [log_response],
})
Common mistakes
- Creating new Client per request — expensive (TLS handshakes).
- No timeout — hung requests pile up.
verify=Falsein production — MITM risk.- Mixing sync and async client.
Read this next
If you want my httpx + tenacity production client, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .