A direct comparison of Rust's three main HTTP client crates, reqwest, ureq, and hyper, with clear guidance on which to choose for your project in 2026. Covers async vs blocking, compile times, binary size, and real-world use cases.
If you need the shortest honest answer: use reqwest by default, use ureq when simplicity and compile time matter more than async, and use hyper only when you are building lower-level HTTP infrastructure.
By Max Wells, updated August 2026
TL;DR:
reqwestis the correct default for async applications;ureqis the correct choice for blocking/synchronous applications and when compile times matter;hyperis for building HTTP libraries and servers, not for direct use as a client.
- reqwest: async, ergonomic, full-featured, the default for 90% of use cases
- ureq: blocking/sync, fast compile times, small binary, correct for CLI tools and scripts
- hyper: low-level HTTP building block, not an end-user client; use reqwest which wraps it
- Compile time: ureq compiles significantly faster than reqwest (reqwest pulls in TLS + tokio)
- Feature parity: reqwest has more built-in features (cookie jar, multipart, redirects); ureq is more minimal
Who Should Read This?
This comparison is for Rust developers choosing an HTTP client for a new project or evaluating whether to switch from their current choice. HTTP client selection is one of the first decisions in any Rust project that makes network requests; it affects your dependency tree, compile times, and async vs sync model. Getting this choice right from the start saves refactoring later.
Bottom line: Best default for application code:
reqwest. Best minimal choice for sync tools:ureq. Best low-level building block:hyper.
Which One Should You Choose in 2026?
Choose reqwest for most application code. Choose ureq when blocking I/O, tiny binaries, and fast compile times matter. Choose hyper only when you need low-level control or are building HTTP infrastructure, not just consuming an API.
Use this quick filter:
- Choose
reqwestif you are already in Tokio or writing any normal async backend or app code. - Choose
ureqif you are writing a CLI, build script, installer, or simple tool where async would only add weight. - Choose
hyperif you are building a framework, a transport layer, or a specialized client where the abstraction cost ofreqwestgets in the way.
The Three Crates: What They Are
reqwest
The ergonomic, full-featured async HTTP client. Built on top of hyper, supports TLS via rustls or native-tls, automatic JSON with serde, cookie management, multipart, and more.
// async usage
let client = reqwest::Client::new();
let response = client
.get("https://api.example.com/users")
.header("Authorization", "Bearer token")
.send()
.await?;
let users: Vec<User> = response.json().await?;ureq
The minimal, blocking HTTP client. No async, no runtime dependency. Simple and fast to compile.
// blocking usage
let response = ureq::get("https://api.example.com/users")
.set("Authorization", "Bearer token")
.call()?;
let users: Vec<User> = response.into_json()?;hyper
Low-level HTTP/1.1 and HTTP/2 implementation. The foundation that reqwest and Axum are built on. Not intended for direct use as a client; use reqwest instead.
// hyper direct usage : verbose, rarely needed
use hyper::{Body, Client, Request};
let client = Client::new();
let req = Request::builder()
.uri("http://api.example.com/users")
.body(Body::empty())?;
let resp = client.request(req).await?;Decision Matrix
| Criterion | reqwest | ureq | hyper |
|---|---|---|---|
| Async | ✅ Yes (tokio) | ❌ Blocking | ✅ Yes |
| Sync/blocking | ✅ Via feature flag | ✅ Default | ❌ Async only |
| Compile time | Slow (TLS + tokio) | Fast | Medium |
| Binary size | Large | Small | Medium |
| JSON built-in | ✅ .json::<T>() | ✅ .into_json::<T>() | ❌ Manual |
| Cookie jar | ✅ | ✅ Limited | ❌ |
| Multipart | ✅ | ✅ | ❌ |
| Redirect handling | ✅ Automatic | ✅ Automatic | ❌ Manual |
| TLS options | rustls or native-tls | rustls or native-tls | Depends on connector |
| Connection pooling | ✅ | ❌ (one-shot) | ✅ |
| HTTP/2 | ✅ | ❌ | ✅ |
| WebSocket upgrade | ❌ (use tungstenite) | ❌ | ✅ Manual |
| Intended use | Application HTTP client | Application HTTP client | Library building block |
Bottom line:
reqwestis the correct default for 90% of Rust projects. Only reach forureqwhen compile times are a bottleneck (CLI tools, build scripts), and only usehyperdirectly if you are building an HTTP library or framework yourself.
When to Use reqwest
Choose reqwest when you are in an async application (tokio-based) and want the least friction.
reqwest is the right choice for:
- Any Axum or Actix-web service that makes outbound HTTP calls
- Microservices talking to external APIs
- Any application already using Tokio
- Projects where ergonomics matter more than compile times
// reqwest shines in async contexts
#[derive(Deserialize)]
struct WeatherResponse {
temperature: f64,
condition: String,
}
async fn get_weather(city: &str) -> anyhow::Result<WeatherResponse> {
let url = format!("https://api.weather.example/{}", city);
let response = reqwest::get(&url).await?.error_for_status()?;
Ok(response.json::<WeatherResponse>().await?)
}reqwest's killer feature for production: the .error_for_status()? method, which returns an error if the HTTP status is 4xx or 5xx, which is what you almost always want and saves boilerplate.
reqwest Compile Time Warning
reqwest is one of the heaviest dependencies in the Rust ecosystem. A project that only uses reqwest (no other heavy deps) will add 30–60 seconds to clean build times on a modern machine. In a large project where you're already compiling tokio and axum, the marginal cost is smaller.
Mitigation: enable reqwest with only the features you need:
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }Bottom line: For async web services and microservices,
reqwestwithdefault-features = false, features = ["json", "rustls-tls"]gives you the best balance of ergonomics and dependency weight.
3 spots open this month → Check if you are eligible.
We help experienced developers transition into Rust roles at €80K–€150K+ in Europe or $130K–$200K+ in the US.
When to Use ureq
Choose ureq when you are building a CLI tool, a blocking application, or when compile times are a priority.
ureq is the right choice for:
- CLI tools that make HTTP calls (compile time matters for developer experience)
- Lambda functions or other environments where cold start time matters
- Applications with no async runtime (simple scripts, build tools, offline-capable tools)
- Any project where binary size is constrained
// ureq: minimal and direct
fn fetch_latest_version(crate_name: &str) -> anyhow::Result<String> {
let url = format!("https://crates.io/api/v1/crates/{}", crate_name);
let response = ureq::get(&url)
.set("User-Agent", "my-tool/0.1")
.call()?;
let data: CratesResponse = response.into_json()?;
Ok(data.crate_info.max_version)
}ureq compiles faster because it does not pull in Tokio. A project using only ureq for HTTP compiles in seconds; the same project using reqwest takes much longer due to the async runtime and TLS dependencies.
ureq Limitations
- No connection pooling by default: each request opens a new TCP connection. For high-frequency requests to the same host, this is inefficient. Use reqwest if you need connection reuse.
- No async: if your application uses Tokio, calling ureq from async code requires
tokio::task::spawn_blocking. - HTTP/2 not supported: HTTP/2 requires reqwest or hyper.
When to Use hyper (Rarely)
You almost never use hyper directly as a client. Use reqwest instead.
hyper is the right choice when:
- You are building an HTTP library or framework that needs HTTP primitives (this is what Axum and reqwest do)
- You need to implement a custom HTTP proxy with full control over the connection lifecycle
- You need HTTP/2 server push or other low-level protocol features
For 99% of Rust developers, the right answer is: use reqwest or ureq, not hyper directly.
Code Comparison: The Same Request in All Three
Fetch JSON from an API, deserialize into a struct:
// reqwest (async)
async fn get_user_reqwest(id: u64) -> anyhow::Result<User> {
let url = format!("https://api.example.com/users/{}", id);
let user = reqwest::get(&url).await?.json::<User>().await?;
Ok(user)
}
// ureq (blocking)
fn get_user_ureq(id: u64) -> anyhow::Result<User> {
let url = format!("https://api.example.com/users/{}", id);
let user: User = ureq::get(&url).call()?.into_json()?;
Ok(user)
}
// hyper (async, low-level : for comparison only)
async fn get_user_hyper(id: u64) -> anyhow::Result<User> {
use hyper::{body::to_bytes, Client, Uri};
let client = Client::new();
let uri: Uri = format!("http://api.example.com/users/{}", id).parse()?;
let resp = client.get(uri).await?;
let bytes = to_bytes(resp.into_body()).await?;
let user: User = serde_json::from_slice(&bytes)?;
Ok(user)
}The hyper version requires manual body buffering and JSON parsing. reqwest and ureq handle this in one line. This is why you use reqwest/ureq, not hyper directly.
Dependency Tree Impact
If you care about compile times and dependency counts:
| Crate | Direct dependencies | Full dep tree (approx) | Compile time (clean) |
|---|---|---|---|
| ureq | ~8 | ~25 | ~5–10 sec |
| reqwest (minimal features) | ~15 | ~60 | ~25–40 sec |
| reqwest (default features) | ~20 | ~80 | ~35–55 sec |
For a CLI tool where compile time is part of the developer experience: ureq's advantage is real and meaningful.
Frequently Asked Questions
Yes, with tokio::task::spawn_blocking:
let result = tokio::task::spawn_blocking(|| {
ureq::get("https://api.example.com/data").call()?.into_json::<Data>()
}).await??;This is the correct pattern but adds overhead. If you are already using Tokio, reqwest is cleaner.
isahc uses libcurl under the hood (C dependency). surf is an async HTTP client with a nice API but lower adoption than reqwest. Neither is the community default in 2026. For new projects, reqwest (async) or ureq (blocking) are the established choices.
rustls: Pure Rust TLS implementation. No system library dependency; the binary includes everything. Recommended for most projects because it is portable and has no version compatibility issues with system OpenSSL.
native-tls: Uses the OS's TLS library (OpenSSL on Linux, SChannel on Windows, SecureTransport on macOS). Smaller binary, uses system trust store. Required if you need client certificate authentication via system keystore.
For most projects: features = ["rustls-tls"] in reqwest, or ureq's default (rustls).
Yes, reqwest::blocking provides a synchronous API. But it still pulls in all of reqwest's dependencies including Tokio (it spawns a runtime internally). If compile times matter, ureq is still smaller than reqwest::blocking.
reqwest errors implement std::error::Error and compose well with anyhow or thiserror. The .error_for_status()? pattern is idiomatic for rejecting 4xx/5xx responses. For production services, wrapping reqwest errors in your own domain error type (e.g. ExternalApiError) gives you cleaner error reporting and easier testing. With anyhow, you get backtraces and context with .context("calling weather API")?, useful when debugging cascading failures in a microservice.
Use ClientBuilder to set per-client timeouts:
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.connect_timeout(std::time::Duration::from_secs(3))
.build()?;Always set timeouts in production. The default is no timeout, which means a slow external API can hang your service indefinitely. Cloudflare's internal Rust services set aggressive connect timeouts (2–3 seconds) and longer read timeouts (10–30 seconds) depending on the downstream service SLA.
Not out of the box; reqwest does not include retry logic. The standard approach is to use the tower middleware stack with tower::retry::Retry when making requests from an Axum service, or use the backon crate (0.4+) for a simpler retry abstraction. For production services calling external APIs, exponential backoff with jitter and a maximum of 3 retries is the standard pattern. Libraries like reqwest-middleware (built on reqwest + tower) give you a hook-based middleware chain for retries, logging, and metrics without reimplementing request dispatch.
reqwest 0.12 (released 2024) updated to hyper 1.0, which is a major breaking change in the underlying HTTP implementation. hyper 1.0 has a significantly different API with a more modular design. The reqwest user-facing API changed minimally, but if you have any direct hyper usage alongside reqwest, you need both to be on the same major version. For new projects in 2026, use reqwest 0.12; all major ecosystem crates (Axum 0.7+, tower-http) have aligned on hyper 1.0.
The most common mistake with reqwest is using it for synchronous CLI tools and being surprised by compile times. reqwest pulls in the full Tokio runtime, TLS stack, and a large dependency tree even if you only make one HTTP call at startup. For CLI tools where the user runs the binary frequently (like a GitHub CLI clone or a deployment tool), the compile time hit affects every developer on the team every time they iterate. ureq 3.x gives you nearly identical ergonomics with 5–10x faster clean builds.
A second common mistake: creating a new reqwest::Client per request. The Client holds a connection pool; creating it once and reusing it is critical for performance. The idiomatic pattern is to construct the client once (in main or in your AppState) and clone it into handlers. Cloning a reqwest::Client is cheap (it's an Arc internally).
A third mistake: ignoring the TLS feature flags. reqwest defaults to native-tls on some platforms, which can introduce system library version issues in Docker containers. Explicitly enabling rustls-tls and disabling native-tls in Cargo.toml gives you reproducible builds regardless of the OS TLS library version.
Developers who understand the Rust HTTP ecosystem, including when to use reqwest vs ureq, how to configure TLS, and how to handle retries and timeouts, are better equipped for senior backend and platform engineering roles. Most Rust backend job postings in 2026 list reqwest alongside sqlx and tokio as core competencies. Platform engineers at companies building internal tools and automation in Rust (common at fintech and infrastructure companies) typically earn $150,000–$200,000 in US markets, with HTTP client expertise being table stakes for these roles.
Keep Reading
- Async Rust and Tokio: The Complete Guide
- Building a Backend API with Axum: A Complete Guide
- Rust Error Handling with Result and Option
- Rust Serde: Serialization and Deserialization Guide
