Python Requests vs httpx: Choosing the Right HTTP Client
A practical comparison of python requests vs httpx covering async support, HTTP/2, timeout defaults, and connection reuse to guide your HTTP client choice.
When a Python service needs to make HTTP calls, requests and httpx are the two libraries most developers reach for. The practical question in any python requests vs httpx comparison is not which library is better in the abstract, but which one fits the runtime model and feature requirements of your project. They expose nearly identical request APIs, which makes the choice look superficial at first. The real differences show up in runtime behavior: async support, HTTP/2, timeout defaults, and connection lifecycle.
The Core Difference: Sync vs Async
requests is strictly synchronous. Every call blocks the calling thread until the response is received. That is fine for scripts, small tools, and request handlers that do not need concurrency, but it becomes a bottleneck when a service must make many parallel HTTP calls.
httpx provides both a synchronous API and an async API. The sync interface mirrors requests closely, so code that uses requests can often be ported with minimal changes. The async interface uses AsyncClient and works with asyncio:
import httpx import asyncio async def fetch(url: str) -> str: async with httpx.AsyncClient() as client: response = await client.get(url) return response.text asyncio.run(fetch("https://example.com"))
The async path matters when the surrounding application is already event-driven. A FastAPI or aiohttp service, for example, should not block the event loop with a synchronous HTTP call. Using AsyncClient keeps the loop responsive while requests are in flight.
The sync path in httpx exists so that the same library can be used in both contexts. That reduces the need to maintain two different HTTP stacks in a codebase that has both synchronous and asynchronous parts.
How Similar the APIs Actually Are
The request API of httpx is deliberately modeled after requests. Common operations look almost identical:
import requests import httpx # requests response = requests.get("https://api.example.com/users", params={"page": 2}) print(response.status_code, response.json()) # httpx (sync) response = httpx.get("https://api.example.com/users", params={"page": 2}) print(response.status_code, response.json())
Headers, query parameters, JSON payloads, cookies, and redirect handling follow the same naming conventions. A developer moving from requests to httpx will find that most existing code needs only the import line changed.
The differences appear in areas where httpx made different design decisions. httpx returns a Response object with a .json() method, but it also exposes .read(), .stream(), and .raise_for_status() in the same way. The main API surface is intentionally compatible, so migration is mostly mechanical.
One notable difference is that httpx supports typed responses and request models through its Client interface, which makes it easier to build reusable request wrappers in larger codebases.
Timeout Behavior: A Practical Difference
requests does not enforce a default timeout. A request can hang indefinitely if the server never responds, which is a common source of stuck workers and unresponsive services. Developers must remember to pass timeout= explicitly on every call.
httpx defaults to a 5-second timeout. That default prevents a single slow endpoint from blocking a thread or event loop indefinitely. The timeout can be configured per request or on the client:
import httpx client = httpx.Client(timeout=10.0) response = client.get("https://api.example.com/slow")
The default timeout is a meaningful operational difference. In a production service, an unresponsive upstream can otherwise tie up connection pool slots and cause cascading failures. httpx's default reduces that risk without requiring every call site to specify a timeout.
The timeout can also be set to None to disable it, matching the requests behavior when that is explicitly desired.
HTTP/2 Support
requests is built on urllib3 and supports only HTTP/1.1. httpx supports HTTP/2 when the optional h2 package is installed. HTTP/2 allows multiplexed requests over a single connection, which reduces connection setup overhead when a client makes many requests to the same host.
import httpx client = httpx.Client(http2=True) response = client.get("https://api.example.com/data")
HTTP/2 is not a default in httpx; it must be enabled explicitly. The benefit is most visible in high-throughput scenarios where many requests go to the same origin. For a script that makes a handful of calls, the difference is negligible.
The tradeoff is that HTTP/2 support adds a dependency and slightly more connection-management complexity. If the target server does not support HTTP/2, the client falls back to HTTP/1.1 automatically.
Client Lifecycle and Connection Reuse
Both libraries reuse connections through a connection pool, but they differ in how the pool is managed. requests creates a new session per call when you use the module-level requests.get(). Each call sets up a new connection unless you explicitly reuse a Session.
httpx encourages creating a Client and reusing it:
import httpx with httpx.Client() as client: for url in urls: response = client.get(url)
Reusing a client keeps the connection pool warm, avoids repeated TLS handshakes, and preserves HTTP keep-alive connections. The same pattern exists in requests with Session, but httpx makes it the natural usage pattern rather than an optimization.
In an async context, the same principle applies to AsyncClient. Creating a new client for every request defeats connection reuse and adds overhead, so a long-lived client is the correct pattern in services that make frequent calls.
Streaming and Large Responses
Both libraries support streaming responses, but the API differs. In requests, streaming is enabled with stream=True and the content is read incrementally:
import requests with requests.get("https://api.example.com/large", stream=True) as response: for chunk in response.iter_content(chunk_size=8192): process(chunk)
In httpx, streaming is handled through a stream() context manager:
import httpx with httpx.stream("GET", "https://api.example.com/large") as response: for chunk in response.iter_bytes(): process(chunk)
The httpx approach makes it explicit that the response body is not loaded into memory. This matters when downloading large files or processing server-sent events, where loading the full body would consume excessive memory.
Choosing Between requests and httpx
The decision depends on the runtime model and the features the project actually needs.
Use requests when the codebase is entirely synchronous, the HTTP workload is light, and the team already relies on requests-specific extensions or middleware. It is stable, mature, and has the largest ecosystem of integrations.
Use httpx when any of the following apply:
- The application uses
asyncioand needs non-blocking HTTP calls. - HTTP/2 multiplexing would reduce latency for many requests to the same host.
- A default timeout should protect the service from hanging upstreams.
- The codebase wants a single HTTP client for both sync and async paths.
A mixed codebase is where httpx has the clearest advantage. A service that has both synchronous background jobs and async request handlers can use one library for both, with the sync and async APIs sharing the same request semantics. Maintaining two separate HTTP stacks, one requests-based and one async-based, duplicates configuration and error-handling logic.
The migration path from requests to httpx is straightforward for most code because the request API was designed to be compatible. The places that need attention are streaming, timeout handling, and client lifecycle, where the two libraries made different design choices.