---
title: How to Make Async HTTP Requests with reqwest
summary: Make async HTTP requests in Rust by adding reqwest to Cargo.toml and using await with reqwest::get() inside an async function.
lang: en
slug: how-to-make-async-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-async-http-requests-with-reqwest/
---

# How to Make Async HTTP Requests with reqwest



## When the thread shouldn't wait

You're used to `fetch` in JavaScript or `requests.get` in Python. You write a line, you get data. In Rust, you write `reqwest::get`, the compiler screams about async contexts, and suddenly you need a runtime. The mental model shifts from "blocking the thread" to "handing work to an event loop."

Rust's `reqwest` crate is the standard for HTTP clients. It embraces Rust's async ecosystem. This means requests don't freeze your thread while waiting for the network. Instead, they yield control back to the runtime, allowing your program to handle thousands of concurrent connections with minimal memory overhead.

## The async mental model

Async code in Rust is built on the `Future` trait. A `Future` is a state machine that tracks progress toward a result. It doesn't do work on its own. It needs an executor to poll it.

Think of a busy restaurant kitchen. A waiter takes an order and hands it to the chef. The waiter doesn't stand at the stove waiting for the food. The waiter takes more orders, checks on other tables, and returns when the chef signals the dish is ready. The waiter is the runtime. The order ticket is the `Future`. The `.await` keyword is the waiter stepping away to do other work until the ticket is marked done.

In Rust, `#[tokio::main]` sets up the waiter. Functions marked `async` return tickets. The `.await` expression tells the waiter to pause this ticket and resume it later.

Async code doesn't run by itself. It needs an executor to drive the state machine.

## Minimal example

Here is the smallest working program. It fetches a page and prints the body.

```rust
use reqwest;

// The tokio runtime drives async tasks. Without this, futures never execute.
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // reqwest::get returns a Future. .await yields control until the request completes.
    let response = reqwest::get("https://www.rust-lang.org").await?;
    
    // Reading the body is also async. The data streams in chunks.
    let body = response.text().await?;
    
    println!("{body}");
    Ok(())
}
```

Add these dependencies to your `Cargo.toml`. The `json` feature enables JSON parsing methods. `tokio` provides the runtime.

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

The compiler rejects `.await` outside an async function with a "cannot await in a non-async context" error. If you forget the runtime attribute, you'll see errors about futures not being executed.

## How the request flows

When you call `reqwest::get`, the function builds a request object and returns a `Future`. Nothing happens yet. No DNS lookup, no connection.

When you `.await`, the runtime takes the future and polls it. The future performs the first step: DNS resolution. If the DNS server is busy, the future registers a callback with the runtime and returns "not ready". The runtime switches to another task.

When the DNS response arrives, the runtime wakes the future. It resumes execution. The future opens a TCP connection. If the connection is pending, it yields again. This continues through the TLS handshake and the HTTP request.

Once the server responds, the future resumes. The `Response` object contains headers and a stream for the body. Calling `.text().await` reads the stream until the end, buffering the content into a `String`.

The `.await` keyword is a suspension point, not a block.

## The Client pattern

The minimal example uses `reqwest::get`. This is a convenience function that creates a new client, sends the request, and drops the client. In real applications, you should reuse a `Client`.

Creating a client sets up connection pools, TLS session caches, and default headers. Reusing the client avoids the overhead of re-establishing these resources for every request.

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

#[derive(Deserialize)]
struct User {
    id: u64,
    name: String,
}

/// Fetches a user from the API and prints details.
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Reusing a Client enables connection pooling and keeps TLS sessions alive.
    let client = reqwest::Client::new();
    
    // Build the request explicitly. This gives you control over headers and timeouts.
    let response = client
        .get("https://jsonplaceholder.typicode.com/users/1")
        .send()
        .await?;
    
    // Check status before parsing. reqwest doesn't error on 4xx/5xx by default.
    if !response.status().is_success() {
        return Err("Request failed".into());
    }
    
    let user: User = response.json().await?;
    println!("User: {} ({})", user.name, user.id);
    Ok(())
}
```

Add `serde` to your dependencies for JSON parsing.

```toml
serde = { version = "1", features = ["derive"] }
```

Cloning a `Client` is cheap. It shares the underlying connection pool via reference counting. The community convention is to create one client per application and clone it wherever you need to make requests.

Clone the client, not the connection pool.

## Errors and status codes

`reqwest` distinguishes between network errors and HTTP errors. A network error occurs when DNS fails, the connection drops, or TLS negotiation fails. These return `Err` from `.await`.

An HTTP error like 404 or 500 is a valid response from the server. `reqwest` returns `Ok(Response)` for these. You must check the status code manually. This differs from Python's `requests`, which can raise exceptions on HTTP errors.

```rust
let response = client.get(url).await?;

// Network succeeded, but the server might have returned an error.
if response.status().is_server_error() {
    eprintln!("Server error: {}", response.status());
    return Err("Server failed".into());
}
```

Always check the status code. A 404 is success for the network, failure for the logic.

## Pitfalls

Blocking the executor is the most common mistake. If you call a blocking function like `std::thread::sleep` or a synchronous database driver inside an async function, you freeze the runtime thread. Other tasks can't make progress.

Use async-compatible alternatives. For sleeping, use `tokio::time::sleep`. For databases, use async drivers like `sqlx`.

Another pitfall involves thread safety. The default `tokio` runtime is multi-threaded. Futures must be `Send` to move between threads. If you capture a `Rc` inside an async block, the compiler rejects it because `Rc` is not `Send`.

The compiler rejects this with `E0277` (the trait bound `Rc<T>: Send` is not satisfied). Use `Arc` instead of `Rc` in async code. `Arc` is thread-safe.

Never call blocking code inside an async function. The runtime will stall.

## Decision matrix

Use `reqwest` with `async`/`await` when your application handles concurrent requests or integrates with an async ecosystem like a web server. Use `reqwest::blocking` when you are writing a simple script, a migration tool, or a CLI command where synchronous flow is clearer and concurrency adds no value. Use `ureq` when dependency size matters and you need a lightweight HTTP client without the overhead of an async runtime. Reach for `hyper` when you are building a server or require fine-grained control over the HTTP protocol implementation.

Pick the tool that matches your concurrency needs, not your nostalgia for synchronous code.

## 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 HTTP Requests in Rust (reqwest)](/en/how-to-make-http-requests-in-rust-reqwest/)
- [How to make HTTP requests with reqwest](/en/how-to-make-http-requests-with-reqwest/)