Blazing fast http-client for Python in Rust π¦ that can be used as drop-in replacement for httpx and requests in most cases.
- Fast:
httpris built on top ofreqwests, which is a blazing fast http client in Rust. Check out the benchmark. - Both async and sync:
httprprovides both a sync and async client. - Lightweight:
httpris a lightweight http client with zero python-dependencies. - Async: first-class async support.
- Streaming: supports streaming responses for efficient memory usage with large payloads.
- http2:
httprsupports HTTP/2. - mTLS:
httprsupports mTLS.
- Fine-grained error handling: Fine-grained error handling is not implemented yet.
π Full documentation: thomasht86.github.io/httpr
π€ LLM-friendly docs: llms.txt | llms-full.txt
uv add httpror
uv pip install httprpip install -U httprPerformance is tracked continuously in two complementary ways:
- CPU cost per pull request with CodSpeed:
tests/benchmark/codspeed/runs under CodSpeed's CPU simulation instrument on every PR and every push tomain, so a regression in decoding, request building or transfer handling is reported on the PR that introduces it. See tests/benchmark/codspeed/README.md. - Wall-clock trend over time with github-action-benchmark: on every push to
main,tests/benchmark/test_performance.pyruns in CI and the results are published as interactive charts.
π View live benchmark results
Against other clients, benchmark/benchmark.py is run every Monday against the latest release of each library (requests, httpx, httpx2, aiohttp, curl_cffi, pycurl, tls_client) and published at thomasht86.github.io/httpr/dev/bench/compare, with the full tables and raw CSVs. Seconds for 400 requests against a local server, lower is better:
To run the benchmarks locally, see benchmark/README.md.
class Client:
"""Initializes an HTTP client.
Args:
auth (tuple[str, str| None] | None): Username and password for basic authentication. Default is None.
auth_bearer (str | None): Bearer token for authentication. Default is None.
params (QueryParamTypes | None): Default query parameters to include in all requests, merged with
each request's own `params` (the request wins per key). Default is None.
headers (dict[str, str] | None): Default headers to send with requests.
cookies (dict[str, str] | None): - Map of cookies to send with requests as the `Cookie` header.
timeout (float | None): Timeout in seconds for waiting on the server: for the response headers,
then for each chunk of the body, so a response that keeps arriving is never cut off. Default is 30;
`None` disables it.
cookie_store (bool | None): Enable a persistent cookie store. Received cookies will be preserved and included
in additional requests. Default is True.
referer (bool | None): Enable or disable automatic setting of the `Referer` header. Default is True.
proxy (str | None): Proxy URL for HTTP requests. Example: "socks5://127.0.0.1:9150". Default is None.
follow_redirects (bool | None): Whether to follow redirects. Default is True.
max_redirects (int | None): Maximum redirects to follow. Default 20. Applies if `follow_redirects` is True.
verify (bool | None): Verify SSL certificates. Default is True.
ca_cert_file (str | None): Path to CA certificate store. Default is None.
https_only` (bool | None): Restrict the Client to be used with HTTPS only requests. Default is `false`.
http2_only` (bool | None): If true - use only HTTP/2; if false - use only HTTP/1. Default is `false`.
"""A Client owns a connection pool. Use it as a context manager, or call close() when you are done, to release the pooled connections back to the OS:
with httpr.Client() as client:
client.get("https://httpbin.org/get")
# pool released here
client = httpr.Client()
try:
client.get("https://httpbin.org/get")
finally:
client.close() # idempotentRequests that are still in flight when close() is called (including open stream() responses) finish normally; the pool is released as soon as the last of them completes. A request made afterwards reopens the client with a fresh pool, the way a requests.Session does, and emits httpr.ClientReopenedWarning (a ResourceWarning, silent by default). Run with -W error::httpr.ClientReopenedWarning or warnings.simplefilter("error", httpr.ClientReopenedWarning) to get httpx's strict use-after-close behaviour instead. client.is_closed tells you which state a client is in. AsyncClient offers the same via aclose() / async with, and additionally shuts down its own thread pool. A client that is garbage-collected without being closed releases its pool too, but only once the interpreter gets to it, so prefer closing explicitly.
The Client class provides a set of methods for making HTTP requests: get, head, options, delete, post, put, patch, each of which internally utilizes the request() method for execution. The parameters for these methods closely resemble those in httpx.
def get(
url: str,
params: QueryParamTypes | None = None,
headers: dict[str, str] | None = None,
cookies: dict[str, str] | None = None,
auth: tuple[str, str| None] | None = None,
auth_bearer: str | None = None,
timeout: float | None = 30,
):
"""Performs a GET request to the specified URL.
Args:
url (str): The URL to which the request will be made.
params (QueryParamTypes | None): Query parameters to append to the URL, merged with the client's.
Values may be str, int, float, bool (sent as `true`/`false`), None (sent empty) or a
list/tuple of those, which repeats the key. Default is None.
headers (dict[str, str] | None): A map of HTTP headers to send with the request. Default is None.
cookies (dict[str, str] | None): Cookies to send with the request, merged with the client's into
a single `Cookie` header.
auth (tuple[str, str| None] | None): A tuple containing the username and an optional password
for basic authentication. Default is None.
auth_bearer (str | None): A string representing the bearer token for bearer token authentication. Default is None.
timeout (float | None): The timeout for the request in seconds. Default is 30.
"""def post(
url: str,
params: QueryParamTypes | None = None,
headers: dict[str, str] | None = None,
cookies: dict[str, str] | None = None,
content: bytes | None = None,
data: dict[str, Any] | None = None,
json: Any | None = None,
files: dict[str, str] | None = None,
auth: tuple[str, str| None] | None = None,
auth_bearer: str | None = None,
timeout: float | None = 30,
):
"""Performs a POST request to the specified URL.
Args:
url (str): The URL to which the request will be made.
params (QueryParamTypes | None): Query parameters to append to the URL, merged with the client's.
Values may be str, int, float, bool (sent as `true`/`false`), None (sent empty) or a
list/tuple of those, which repeats the key. Default is None.
headers (dict[str, str] | None): A map of HTTP headers to send with the request. Default is None.
cookies (dict[str, str] | None): Cookies to send with the request, merged with the client's into
a single `Cookie` header.
content (bytes | None): The content to send in the request body as bytes. Default is None.
data (dict[str, Any] | None): The form data to send in the request body; values are converted
like `params`, so a list repeats the field. Default is None.
json (Any | None): A JSON serializable object to send in the request body. Default is None.
files (dict[str, str] | None): A map of file fields to file paths to be sent as multipart/form-data. Default is None.
auth (tuple[str, str| None] | None): A tuple containing the username and an optional password
for basic authentication. Default is None.
auth_bearer (str | None): A string representing the bearer token for bearer token authentication. Default is None.
timeout (float | None): The timeout for the request in seconds. Default is 30.
"""The Client class returns a Response object that contains the following attributes and methods:
resp.content
resp.cookies
resp.encoding
resp.headers
resp.json()
resp.status_code
resp.reason_phrase # e.g. "OK", "Not Found"
resp.raise_for_status() # raise HTTPStatusError on non-2xx (returns self)
resp.is_informational # 1xx
resp.is_success # 2xx
resp.is_redirect # 3xx
resp.is_client_error # 4xx
resp.is_server_error # 5xx
resp.is_error # 4xx or 5xx
resp.has_redirect_location # 3xx with a Location header
resp.text
resp.text_markdown # html is converted to markdown text using html2text-rs
resp.text_plain # html is converted to plain text
resp.text_rich # html is converted to rich text
resp.urlraise_for_status() follows httpx semantics: any non-2xx status raises
HTTPStatusError (not just 4xx/5xx as in requests).
The Client class supports streaming responses for efficient memory usage when handling large payloads. Use the stream() context manager to iterate over response data without buffering the entire response in memory.
# Stream bytes chunks
with client.stream("GET", "https://example.com/large-file") as response:
print(f"Status: {response.status_code}")
for chunk in response.iter_bytes():
process(chunk)
# Stream text chunks
with client.stream("GET", "https://example.com/text") as response:
for text in response.iter_text():
print(text, end="")
# Stream line by line (useful for Server-Sent Events)
with client.stream("GET", "https://example.com/events") as response:
for line in response.iter_lines():
print(line.strip())
# Read entire response (if needed after checking headers)
with client.stream("GET", url) as response:
if response.status_code == 200:
content = response.read()StreamingResponse attributes:
status_code- HTTP status codereason_phrase- Canonical reason phrase (e.g. "OK")raise_for_status()- RaiseHTTPStatusErroron a non-2xx status (returns self)is_success/is_error/is_redirect/ ... - Status-class helpers (same asResponse)headers- Response headers (case-insensitive)cookies- Response cookiesurl- Final URL after redirectsis_closed- Whether the stream has been closedis_consumed- Whether the stream has been fully consumed
StreamingResponse methods:
iter_bytes()- Iterate over response as bytes chunksiter_text()- Iterate over response as text chunks (decoded using response encoding)iter_lines()- Iterate over response line by lineread()- Read entire remaining response body into memoryclose()- Close the stream and release resources
Important notes:
- Streaming must be used as a context manager (with statement)
- Headers, cookies, and status code are available immediately before reading the body
- The response body is only read when you iterate over it or call
read() - Once consumed, the stream cannot be read again
- Streaming is supported for all HTTP methods (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS)
import httpr
# Initialize the client
client = httpr.Client()
# GET request
resp = client.get("https://tls.peet.ws/api/all")
print(resp.json())
# GET request with passing params and setting timeout
params = {"param1": "value1", "param2": "value2"}
resp = client.post(url="https://httpbin.org/anything", params=params, timeout=10)
print(r.text)
# POST Binary Request Data
content = b"some_data"
resp = client.post(url="https://httpbin.org/anything", content=content)
print(r.text)
# POST Form Encoded Data
data = {"key1": "value1", "key2": "value2"}
resp = client.post(url="https://httpbin.org/anything", data=data)
print(r.text)
# POST JSON Encoded Data
json = {"key1": "value1", "key2": "value2"}
resp = client.post(url="https://httpbin.org/anything", json=json)
print(r.text)
# POST Multipart-Encoded Files
files = {'file1': '/home/root/file1.txt', 'file2': 'home/root/file2.txt'}
r = client.post("https://httpbin.org/post", files=files)
print(r.text)
# Authentication using user/password
auth = ("user", "password")
resp = client.post(url="https://httpbin.org/anything", auth=auth)
print(r.text)
# Authentication using auth bearer
auth_bearer = "bearerXXXXXXXXXXXXXXXXXXXX"
resp = client.post(url="https://httpbin.org/anything", auth_bearer=auth_bearer)
print(r.text)
# Using proxy or env var HTTPR_PROXY
resp = httpr.Client(proxy="http://127.0.0.1:8080").get("https://tls.peet.ws/api/all")
print(resp.json())
export HTTPR_PROXY="socks5://127.0.0.1:1080"
resp = httpr.Client().get("https://tls.peet.ws/api/all")
print(resp.json())
# Using custom CA certificate store: ca_cert_file param, or env var HTTPR_CA_BUNDLE as the default
# (ca_cert_file applies only to the client it is passed to and never modifies the environment)
resp = httpr.Client(ca_cert_file="/cert/cacert.pem").get("https://tls.peet.ws/api/all")
print(resp.json())
resp = httpr.Client(ca_cert_file=certifi.where()).get("https://tls.peet.ws/api/all")
print(resp.json())
export HTTPR_CA_BUNDLE="/home/user/Downloads/cert.pem"
resp = httpr.Client().get("https://tls.peet.ws/api/all")
print(resp.json())
# You can also use convenience functions that use a default Client instance under the hood:
# httpr.get() | httpr.head() | httpr.options() | httpr.delete() | httpr.post() | httpr.patch() | httpr.put()
resp = httpr.get("https://httpbin.org/anything")
print(r.text)httpr.AsyncClient() is an asynchronous wrapper around the httpr.Client class, offering the same functions, behavior, and input arguments.
import asyncio
import logging
import httpr
async def aget_text(url):
async with httpr.AsyncClient() as client:
resp = await client.get(url)
return resp.text
async def main():
urls = ["https://nytimes.com/", "https://cnn.com/", "https://abcnews.go.com/"]
tasks = [aget_text(u) for u in urls]
results = await asyncio.gather(*tasks)
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
asyncio.run(main())Streaming with AsyncClient:
AsyncClient.stream() yields an AsyncStreamingResponse with async for iteration. aiter_bytes(), aiter_text(), aiter_lines() and aread() read each chunk on the client's thread pool, so the event loop keeps serving other tasks while the server is producing data (useful for Server-Sent Events and LLM token streams that stay open for a long time):
async with httpr.AsyncClient() as client:
async with client.stream("GET", "https://example.com/large-file") as response:
async for chunk in response.aiter_bytes():
process(chunk)
async with client.stream("GET", "https://example.com/events") as response:
async for line in response.aiter_lines():
handle(line)The synchronous iter_bytes(), iter_text(), iter_lines() and read() are still available on the async response, but each step blocks the event loop until the next chunk arrives, so prefer the a-prefixed variants in async code.
Provides precompiled wheels for the following platforms:
- π§ linux:
amd64,aarch64,armv7(aarch64 and armv7 builds aremanylinux_2_34compatible.ubuntu>=22.04,debian>=12) - π§ musllinux:
amd64,aarch64 - πͺ windows:
amd64 - π macos:
amd64,aarch64.
This project uses Taskfile for development workflows.
# Install dependencies
uv sync --extra dev
# Build Rust extension (required after any .rs changes)
uv run maturin develop
# Add hosts entry for e2e tests (one-time setup)
echo '127.0.0.1 httpbun.local' | sudo tee -a /etc/hosts# List all available tasks
task --list
# Run unit tests only
task test:unit
# Run e2e tests (full workflow: start httpbun β test β stop)
task e2e
# Run e2e tests iteratively (keep httpbun running)
task e2e:local
task test:e2e # run tests against running container
# Run all tests
task testtask dev # Build Rust extension
task check # Run all checks (lint + test) - use before committing
task test:benchmark # Wall-clock benchmarks
task test:codspeed # CodSpeed CPU-simulation benchmarks (requires the codspeed CLI)
task lint # Run Python linters (ruff + mypy)
task lint:rust # Run Rust linters (fmt + clippy)
task lint:all # Run all linters (Python + Rust)
task fmt # Format Python code with ruff
task fmt:rust # Format Rust code
task fmt:all # Format all code (Python + Rust)
task certs # Generate SSL certificates for e2e tests
task httpbun:start # Start httpbun container
task httpbun:stop # Stop httpbun container
task httpbun:logs # Show container logstests/unit/- Unit tests using pytest-httpbin (fast, no Docker required)tests/e2e/- E2E tests using httpbun Docker container with SSLtests/benchmark/test_performance.py- Wall-clock benchmarks published to the live chartstests/benchmark/codspeed/- CPU benchmarks measured by CodSpeed on every PR
| Job | PRs | Push to main | Tags (Release) | Manual |
|---|---|---|---|---|
lint |
β | |||
test (Python 3.10-3.14) |
β | β | β | |
docs (build) |
β | |||
benchmarks (CodSpeed) |
β | β | β | |
linux, musllinux, windows, macos, sdist |
β | β | ||
release (PyPI publish) |
β | β |
- PRs: Run lint, tests across Python 3.10-3.14 matrix, verify docs build, and report the CPU cost of the CodSpeed benchmark suite
- Push to main: Run tests and the CodSpeed benchmarks, then the separate
Benchmarkworkflow runs the wall-clock suite and publishes results to the live benchmark charts - Weekly (Monday 06:00 UTC): The
Client comparisonworkflow benchmarks httpr against the latest release of each other client and publishes the comparison page - Tags: Run tests, build wheels, publish stable release to PyPI
- Manual: Full multi-platform wheel builds with release
- uv: The package manager used, and for leading the way in the "Rust for python tools"-sphere.
- primp: A lot of code is borrowed from primp, that wraps rust library
rquestfor python in a similar way. If primp supported mTLS, I would have used it instead. - reqwests: The rust library that powers httpr.
- pyo3
- maturin
