---
title: How to Make HTTP Requests in Rust (reqwest)
summary: Make HTTP requests in Rust by adding the reqwest crate and using async/await to fetch data from URLs.
lang: en
slug: how-to-make-http-requests-in-rust-reqwest
published: 2026-04-17T12:05:42+00:00
updated: 2026-04-25T22:00:13+00:00
canonical: https://www.rustfaq.org/en/how-to-make-http-requests-in-rust-reqwest/
---

# How to Make HTTP Requests in Rust (reqwest)



## The HTTP problem in Rust

You're writing a CLI tool that needs to fetch the latest version number from a GitHub API. Or maybe a backend service that calls a payment provider. In Python, you'd grab `requests` and call it a day. In JavaScript, `fetch` is built-in. Rust doesn't ship with an HTTP client in the standard library. You need a crate. The community standard is `reqwest`.

`reqwest` is the most widely used HTTP client in the Rust ecosystem. It provides a clean, high-level API for making requests. It supports async and blocking modes, JSON serialization, multipart forms, and streaming. It sits on top of `hyper`, the low-level HTTP implementation, and handles the gritty details of connection management so you don't have to.

## How reqwest fits the async model

Rust's concurrency model relies on `async`/`await`. This isn't just a syntax sugar; it's a fundamental shift in how you think about I/O. `reqwest` is async by default. This matches the Rust ecosystem's preference for non-blocking I/O, especially in servers and high-throughput tools.

Think of `reqwest` like a professional courier service. You don't run to the post office yourself and stand there waiting for a reply. You hand the package to the courier, they give you a ticket, and you go back to work. When the courier finishes the delivery, they call you. In Rust terms, `reqwest` hands you a `Future` (the ticket). You call `.await` on it, which tells the runtime, "Pause this function until the network call finishes, then come back here." The runtime can use that pause time to handle other requests. This lets you handle thousands of concurrent connections with minimal memory overhead.

## Minimal example

Start with a simple GET request. You need `reqwest` and an async runtime. `tokio` is the most common runtime choice.

```toml
[dependencies]
reqwest = { version = "0.12", features = ["json"] }
tokio = { version = "1", features = ["full"] }
```

```rust
use reqwest;

// tokio::main sets up the async runtime.
// Without this, you can't use .await in main.
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
    // reqwest::get is a convenience function.
    // It creates a temporary client, sends the request, and returns the response.
    // This returns a Future, not the response itself.
    let response = reqwest::get("https://httpbin.org/get").await?;

    // Read the response body as text.
    // This is another async operation that waits for the full body.
    let body = response.text().await?;

    println!("Status: {}", response.status());
    println!("Body: {}", body);
    Ok(())
}
```

### Walkthrough

1.  `reqwest::get` prepares the request and returns a `Future`. Nothing hits the network yet.
2.  `.await` polls the future. The runtime takes over, performs the DNS lookup, TCP handshake, TLS negotiation, and sends the request. Your function is suspended.
3.  When the response headers arrive, the future resolves. The function resumes.
4.  `response.text()` reads the body. This might require multiple network reads. `.await` handles that too.
5.  The `?` operator propagates errors. If the request fails, `main` returns early with the error.

The `Result<(), reqwest::Error>` return type is standard. `reqwest` methods return `Result<T, reqwest::Error>`. The `?` operator unwraps the `Ok` value or returns the `Err` immediately.

## The Client is the star

The minimal example uses `reqwest::get`. That function is convenient for scripts, but it has a hidden cost. Every call to `reqwest::get` creates a new `Client` under the hood. Creating a client involves setting up connection pools and TLS contexts. Doing this for every request burns CPU and slows down your application.

In real code, you create a `Client` once and reuse it. The client maintains a pool of idle connections. When you make a request, it reuses an existing connection if one is available. This skips the TCP handshake and TLS negotiation, which are expensive operations.

```rust
use reqwest;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create a single client instance.
    // This initializes the connection pool and TLS settings.
    let client = reqwest::Client::new();

    // Reuse the client for multiple requests.
    // The client will reuse connections from the pool.
    let response1 = client.get("https://httpbin.org/get").send().await?;
    let response2 = client.get("https://httpbin.org/headers").send().await?;

    println!("Response 1: {}", response1.status());
    println!("Response 2: {}", response2.status());
    Ok(())
}
```

**Convention aside:** The community calls this the "reuse the client" rule. You'll see `Client` passed around in structs or via dependency injection in larger apps. Never create a new client inside a loop or a request handler.

Reuse the client. Creating a new one per request burns CPU on handshakes and kills throughput.

## Error handling that doesn't lie

HTTP errors are tricky. A network failure is different from a 404 Not Found. `reqwest` treats them differently. If the server returns a 404, `reqwest` considers that a successful network operation. The response body contains the error page. `reqwest` won't return an error automatically.

If you want to treat HTTP error status codes as errors, you must call `error_for_status()`. This method checks the status code and returns an error if it's 4xx or 5xx.

```rust
use reqwest;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = reqwest::Client::new();

    // send() returns the response regardless of status code.
    // error_for_status() converts 4xx/5xx into an Error.
    let response = client
        .get("https://httpbin.org/status/404")
        .send()
        .await?
        .error_for_status()?;

    // This line won't run if the status is 404.
    let body = response.text().await?;
    println!("{}", body);
    Ok(())
}
```

When `error_for_status()` fails, the error contains the status code. You can extract it to provide better error messages.

```rust
match client.get("https://httpbin.org/status/500").send().await {
    Ok(response) => {
        match response.error_for_status() {
            Ok(_) => println!("Success"),
            Err(e) => {
                // Extract the status code from the error.
                if let Some(status) = e.status() {
                    eprintln!("Server error: {}", status);
                } else {
                    eprintln!("Error without status: {}", e);
                }
            }
        }
    }
    Err(e) => eprintln!("Network error: {}", e),
}
```

Call `error_for_status()`. A 404 is a valid response, not a network error. Treat it like one or your error handling is broken.

## JSON and Serde

Most APIs return JSON. `reqwest` integrates with `serde` to parse JSON directly into Rust structs. Enable the `json` feature in `Cargo.toml` (included in the minimal example above).

```rust
use reqwest;
use serde::Deserialize;

#[derive(Deserialize, Debug)]
struct User {
    name: String,
    email: String,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = reqwest::Client::new();

    // json() reads the body and deserializes it into User.
    // This fails if the JSON is invalid or fields are missing.
    let user: User = client
        .get("https://jsonplaceholder.typicode.com/users/1")
        .header("Accept", "application/json")
        .send()
        .await?
        .error_for_status()?
        .json()
        .await?;

    println!("Found user: {:?}", user);
    Ok(())
}
```

**Convention aside:** Always set the `Accept: application/json` header when requesting JSON. Some servers vary their response format based on this header. It's a small detail that prevents hard-to-debug parsing errors.

## Pitfalls and compiler errors

### Forgetting the runtime

If you try to use `reqwest` without a runtime, the compiler stops you. You'll see `E0277` (trait bound not satisfied) complaining that a future cannot be awaited without a runtime.

```text
error[E0277]: `main` function is not allowed to be `async`
```

You need `#[tokio::main]` or an equivalent attribute from another runtime like `async-std`.

### Blocking in async context

`reqwest` is async. You can't call `.await` inside a non-async function. If you try, the compiler rejects it.

```rust
fn fetch_data() -> String {
    // Error: .await is not allowed in a non-async function
    reqwest::get("https://example.com").await.unwrap().text().await.unwrap()
}
```

Make the function async. Or use `reqwest::blocking` if you truly need synchronous code.

### Temporary values and borrows

If you try to borrow data from a response after the response is dropped, you'll hit `E0716` (temporary value dropped while borrowed).

```rust
let body = reqwest::get("https://example.com")
    .await?
    .text()
    .await?;
// The response is dropped here.
// If you tried to borrow from it, this would fail.
```

Store the response in a variable if you need to access it multiple times.

Async is a contract. If you call `await`, you're promising the runtime you'll yield control. Break that promise and the compiler will stop you.

## Decision: when to use reqwest vs alternatives

Use `reqwest` with async features when you're building a server, a CLI tool that can be async, or anything that needs high concurrency. It's the standard choice for most Rust applications.

Use `reqwest::blocking` when you're writing a simple script where async overhead is unnecessary, or you're integrating with a codebase that doesn't use async. Enable the `blocking` feature in `Cargo.toml`.

Reach for `ureq` when you need a minimal dependency footprint and don't care about async. `ureq` is a synchronous client with fewer features but a smaller binary size.

Pick `hyper` when you're building the HTTP server itself or need low-level control over the connection lifecycle. `hyper` is the foundation `reqwest` is built on. Use it only when `reqwest` doesn't give you enough control.

## Where to go next

- [How to use reqwest crate in Rust HTTP client](/en/how-to-use-reqwest-crate-in-rust-http-client/)
- [How to Make Async HTTP Requests with reqwest](/en/how-to-make-async-http-requests-with-reqwest/)
- [How to make HTTP requests with reqwest](/en/how-to-make-http-requests-with-reqwest/)