---
title: How to make HTTP requests with reqwest
summary: Make HTTP requests in Rust by adding the reqwest crate and calling reqwest::get() with an async runtime.
lang: en
slug: how-to-make-http-requests-with-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-with-reqwest/
---

# How to make HTTP requests with reqwest



## How to make HTTP requests with reqwest

You're writing a CLI tool that needs to check if a service is up. Or a scraper that pulls JSON from an API. In Python, you `import requests` and get your data in three lines. In Rust, you reach for `reqwest`, the de facto HTTP client. But the moment you try to run the code, the compiler stops you. You forgot the async runtime. You ignored the error type. You tried to print a response that hasn't arrived yet. HTTP in Rust requires a bit more setup, but that setup gives you control over memory, concurrency, and safety that blocking libraries can't match.

## Async by default

`reqwest` is asynchronous by default. Think of it like ordering at a busy restaurant. A synchronous request is standing at the counter and staring at the kitchen until your food arrives. You can't do anything else. An asynchronous request is getting a buzzer. You hand in your order, get the buzzer, and go sit down. You can read a menu, chat with friends, or check your phone. When the buzzer goes off, you go get your food.

`reqwest` gives you the buzzer. It starts the request and hands you a `Future`. You `await` that future to get the result, but while you're waiting, your program can handle other work. This is how Rust handles I/O without spawning a thread for every request. The runtime multiplexes thousands of connections on a few threads. You write code that looks sequential, but the runtime weaves the execution together.

## Minimal example

Add `reqwest` to your `Cargo.toml`. You also need an async runtime. `tokio` is the standard choice.

```toml
[dependencies]
// Disable default features to avoid native-tls/OpenSSL dependency.
// Enable rustls-tls for a pure-Rust TLS implementation.
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
// reqwest is async. You need a runtime to drive the futures.
tokio = { version = "1", features = ["full"] }
```

Convention aside: `reqwest` enables `native-tls` by default, which pulls in OpenSSL bindings. That works, but `rustls` is pure Rust, faster to compile, and easier to cross-compile. The community standard for new projects is `rustls-tls`. Stick with `rustls` unless you have a specific reason to use `native-tls`.

Here is the smallest working program.

```rust
use reqwest;

/// Fetches a URL and prints the status and body.
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // reqwest::get is a convenience function for simple cases.
    // It returns a Future, so we must await it.
    let response = reqwest::get("https://httpbin.org/get").await?;

    // The response body is also a Future.
    // We await it to read the content into a String.
    let text = response.text().await?;

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

## What happens under the hood

When you call `reqwest::get`, nothing happens on the network yet. The function builds a request object and returns a `Future`. The `await` keyword is where the work starts. It tells the runtime, "Start this task. If it's not done, pause this function and let other tasks run. Come back when the data arrives."

The runtime polls the future. The future connects to the server, sends the HTTP headers, and waits for the response. Once the response headers arrive, the future resolves to a `Response`. You still have to `await` the body. HTTP bodies are streams. `response.text()` reads the stream until EOF and gives you a `String`. If the connection drops or the server returns a 4xx/5xx status, the `?` operator propagates the error up to `main`, which prints it and exits.

The runtime manages the concurrency. You write sequential code. The compiler and runtime handle the rest.

## Realistic example

In real code, you rarely use `reqwest::get`. You create a `Client`. The client holds a connection pool. Reusing the client means you reuse TCP connections and TLS handshakes. That's a massive performance win if you're making multiple requests. You also need to handle JSON. `reqwest` integrates with `serde`. The `.json()` method deserializes the body directly into a struct.

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

#[derive(Deserialize, Debug)]
struct GithubUser {
    login: String,
    id: u64,
}

/// Fetches a GitHub user and deserializes the JSON response.
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create a Client to reuse connections across requests.
    // This avoids the overhead of new TCP/TLS handshakes.
    let client = reqwest::Client::new();

    // Many APIs require a User-Agent header.
    // reqwest does not set one by default.
    let user: GithubUser = client
        .get("https://api.github.com/users/octocat")
        .header("User-Agent", "rust-faq-example")
        // send() returns a Future that resolves to Response.
        .send()
        .await?
        // json() reads the body and deserializes using serde.
        .json()
        .await?;

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

Convention aside: GitHub and many other APIs reject requests without a `User-Agent` header. `reqwest` does not set a default. You must set one, or the server returns 403. Always set a descriptive `User-Agent` in your code.

Reuse the client. Reusing the client reuses connections. That's the single biggest performance win you can get with `reqwest`.

## Sending data

Fetching is half the battle. APIs expect data. `reqwest` makes sending JSON, forms, and files straightforward. The builder pattern handles content negotiation automatically.

```rust
use reqwest;
use serde::Serialize;

#[derive(Serialize)]
struct CreatePost {
    title: String,
    body: String,
}

/// Sends a JSON payload to an API.
async fn send_post(client: &reqwest::Client) -> Result<(), Box<dyn std::error::Error>> {
    let post = CreatePost {
        title: "Hello".into(),
        body: "World".into(),
    };

    // json() serializes the struct and sets Content-Type: application/json.
    // You don't need to set the header manually.
    let response = client
        .post("https://jsonplaceholder.typicode.com/posts")
        .json(&post)
        .send()
        .await?;

    println!("Created: {}", response.status());
    Ok(())
}
```

Use `.json(&data)` to send a serializable struct. `reqwest` converts it to JSON bytes and sets the `Content-Type` header. Use `.form(&data)` for form data. It sets `application/x-www-form-urlencoded`. You never need to set these headers manually. The builder methods do it for you.

Let the builder set the headers. Manual headers are a source of bugs. Trust the builder methods.

## Pitfalls and errors

Network calls are unpredictable. Servers drop connections. DNS fails. If you don't set a timeout, your async task hangs forever, holding a slot in the runtime. `reqwest` doesn't set a default timeout. You must configure it. Use `Client::builder().timeout(Duration::from_secs(10))`. This kills the request if no data arrives for 10 seconds.

Reading the body with `.text()` loads the entire response into memory. That's fine for JSON. It's a disaster for a 500MB video file. For large payloads, use `.stream()` to process chunks as they arrive. This keeps memory usage constant regardless of file size.

Error handling in `reqwest` splits into two categories. Transport errors (DNS failure, timeout) and HTTP errors (404, 500). `reqwest::Error` covers transport. HTTP status codes are in the `Response`. `reqwest::get` returns `Ok(Response)` for 404. If you don't check the status, your code proceeds with empty or error data. Use `response.error_for_status()` to turn non-success codes into errors.

If you forget `await`, the compiler stops you. You'll get a type mismatch error. `reqwest::get` returns a `Future`, not a `Response`. The compiler rejects this with `E0308` (mismatched types). You're trying to assign a `Future` to a variable expecting a `Response`. Add `await` to resolve the future.

Check the status code. `reqwest` won't save you from a 404. Treat HTTP errors like transport errors using `error_for_status()`.

## Decision matrix

Use `reqwest` when you need an async HTTP client with a high-level API. It handles connection pooling, TLS, and JSON serialization out of the box.

Use `ureq` when you need a synchronous HTTP client. It blocks the current thread, which is fine for simple scripts or when you can't use an async runtime.

Use `hyper` when you need to build an HTTP server or require fine-grained control over the HTTP protocol. `reqwest` is built on `hyper`, but `hyper` is lower level and more verbose.

Use `reqwest::blocking` when you are writing a synchronous CLI tool and don't want to pull in an async runtime. It provides the same API but blocks the thread. It's a wrapper around the async client, so you get the same features without `await`.

Use `surf` when you want a web-framework-agnostic async client that works across different runtimes. It's a wrapper around other clients, though `reqwest` is more feature-complete.

Stick with `reqwest` for 99% of client work. It's the ecosystem standard.

## Where to go next

* [What is tokio task JoinSet](/en/what-is-tokio-task-joinset/)
* [How to Implement Structured Concurrency in Rust](/en/how-to-implement-structured-concurrency-in-rust/)
* [How to Handle Backpressure in Async Rust](/en/how-to-handle-backpressure-in-async-rust/)