Table of contents

How can I make concurrent requests using Reqwest's async functionality?

Reqwest's async functionality enables efficient concurrent HTTP requests in Rust applications. By leveraging Rust's async/await syntax with the Tokio runtime, you can perform multiple HTTP requests simultaneously, significantly improving performance compared to sequential requests.

Project Setup

Add the required dependencies to your Cargo.toml:

[dependencies]
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1", features = ["full"] }
futures = "0.3"

Note: Use the default Tokio runtime instead of async-std-runtime for better compatibility.

Basic Concurrent Requests with join_all

The simplest approach uses futures::future::join_all to wait for all requests to complete:

use reqwest;
use tokio;
use futures::future::join_all;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let urls = vec![
        "https://httpbin.org/delay/1",
        "https://httpbin.org/delay/2", 
        "https://httpbin.org/delay/1",
    ];

    let client = reqwest::Client::new();

    // Create futures for all requests
    let futures: Vec<_> = urls.into_iter()
        .map(|url| {
            let client = client.clone();
            async move {
                let response = client.get(url).send().await?;
                let status = response.status();
                let text = response.text().await?;
                Ok::<(reqwest::StatusCode, String), reqwest::Error>((status, text))
            }
        })
        .collect();

    // Execute all requests concurrently
    let results = join_all(futures).await;

    for (i, result) in results.into_iter().enumerate() {
        match result {
            Ok((status, body)) => println!("Request {}: {} - {} bytes", i, status, body.len()),
            Err(e) => eprintln!("Request {} failed: {}", i, e),
        }
    }

    Ok(())
}

Using tokio::spawn for True Parallelism

For better concurrency control and true parallel execution, use tokio::spawn:

use reqwest;
use tokio;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let urls = vec![
        "https://httpbin.org/json",
        "https://httpbin.org/uuid",
        "https://httpbin.org/ip",
    ];

    let client = reqwest::Client::new();
    let mut handles = Vec::new();

    // Spawn tasks for concurrent execution
    for (i, url) in urls.into_iter().enumerate() {
        let client = client.clone();
        let handle = tokio::spawn(async move {
            let start = std::time::Instant::now();
            let result = client.get(url).send().await;
            let duration = start.elapsed();

            match result {
                Ok(response) => {
                    let status = response.status();
                    let json: serde_json::Value = response.json().await?;
                    Ok::<(usize, reqwest::StatusCode, serde_json::Value, std::time::Duration), reqwest::Error>(
                        (i, status, json, duration)
                    )
                }
                Err(e) => Err(e),
            }
        });
        handles.push(handle);
    }

    // Collect results
    for handle in handles {
        match handle.await? {
            Ok((id, status, json, duration)) => {
                println!("Task {}: {} ({:?}) - {:?}", id, status, duration, json);
            }
            Err(e) => eprintln!("Task failed: {}", e),
        }
    }

    Ok(())
}

Controlled Concurrency with Semaphores

To limit the number of concurrent requests and avoid overwhelming servers:

use reqwest;
use tokio;
use tokio::sync::Semaphore;
use std::sync::Arc;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let urls = (1..=20).map(|i| format!("https://httpbin.org/delay/{}", i % 3 + 1)).collect::<Vec<_>>();

    let client = reqwest::Client::new();
    let semaphore = Arc::new(Semaphore::new(5)); // Limit to 5 concurrent requests
    let mut handles = Vec::new();

    for (i, url) in urls.into_iter().enumerate() {
        let client = client.clone();
        let semaphore = semaphore.clone();

        let handle = tokio::spawn(async move {
            let _permit = semaphore.acquire().await.unwrap();

            println!("Starting request {} to {}", i, url);
            let start = std::time::Instant::now();

            let result = client
                .get(&url)
                .timeout(std::time::Duration::from_secs(10))
                .send()
                .await;

            let duration = start.elapsed();

            match result {
                Ok(response) => {
                    println!("Completed request {} in {:?} - Status: {}", i, duration, response.status());
                    Ok(response.text().await?)
                }
                Err(e) => {
                    eprintln!("Request {} failed: {}", i, e);
                    Err(e)
                }
            }
        });

        handles.push(handle);
    }

    // Wait for all tasks to complete
    for handle in handles {
        let _ = handle.await?;
    }

    Ok(())
}

POST Requests with JSON Data

Concurrent POST requests with different payloads:

use reqwest;
use tokio;
use serde_json::json;
use futures::future::join_all;

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

    let payloads = vec![
        json!({"name": "Alice", "age": 30}),
        json!({"name": "Bob", "age": 25}),
        json!({"name": "Charlie", "age": 35}),
    ];

    let futures: Vec<_> = payloads.into_iter()
        .enumerate()
        .map(|(i, payload)| {
            let client = client.clone();
            async move {
                let response = client
                    .post("https://httpbin.org/post")
                    .json(&payload)
                    .send()
                    .await?;

                let status = response.status();
                let response_json: serde_json::Value = response.json().await?;

                Ok::<(usize, reqwest::StatusCode, serde_json::Value), reqwest::Error>(
                    (i, status, response_json)
                )
            }
        })
        .collect();

    let results = join_all(futures).await;

    for result in results {
        match result {
            Ok((id, status, json)) => {
                println!("POST {}: {} - Echoed data: {:?}", 
                    id, status, json.get("json"));
            }
            Err(e) => eprintln!("POST failed: {}", e),
        }
    }

    Ok(())
}

Error Handling and Retry Logic

Robust concurrent requests with retry mechanisms:

use reqwest;
use tokio;
use std::time::Duration;

async fn fetch_with_retry(
    client: &reqwest::Client, 
    url: &str, 
    max_retries: u32
) -> Result<String, reqwest::Error> {
    let mut attempts = 0;

    loop {
        match client.get(url).send().await {
            Ok(response) if response.status().is_success() => {
                return response.text().await;
            }
            Ok(response) => {
                eprintln!("HTTP error {}: {}", response.status(), url);
            }
            Err(e) if attempts < max_retries => {
                eprintln!("Attempt {} failed for {}: {}", attempts + 1, url, e);
            }
            Err(e) => return Err(e),
        }

        attempts += 1;
        if attempts > max_retries {
            return Err(reqwest::Error::from(std::io::Error::new(
                std::io::ErrorKind::TimedOut,
                "Max retries exceeded"
            )));
        }

        // Exponential backoff
        let delay = Duration::from_millis(100 * 2_u64.pow(attempts));
        tokio::time::sleep(delay).await;
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let urls = vec![
        "https://httpbin.org/status/200",
        "https://httpbin.org/status/500", // Will trigger retries
        "https://httpbin.org/status/200",
    ];

    let client = reqwest::Client::builder()
        .timeout(Duration::from_secs(30))
        .build()?;

    let futures: Vec<_> = urls.into_iter()
        .map(|url| fetch_with_retry(&client, url, 3))
        .collect();

    let results = futures::future::join_all(futures).await;

    for (i, result) in results.into_iter().enumerate() {
        match result {
            Ok(content) => println!("Success {}: {} bytes", i, content.len()),
            Err(e) => eprintln!("Failed {}: {}", i, e),
        }
    }

    Ok(())
}

Performance Tips

  1. Reuse Client: Always reuse the same reqwest::Client instance to leverage connection pooling
  2. Tune Connection Pool: Configure the client for your use case:
let client = reqwest::Client::builder()
    .pool_max_idle_per_host(10)
    .pool_idle_timeout(Duration::from_secs(30))
    .timeout(Duration::from_secs(30))
    .build()?;
  1. Use Semaphores: Limit concurrent requests to avoid overwhelming servers
  2. Implement Backoff: Use exponential backoff for retries
  3. Handle Timeouts: Set appropriate timeouts for different request types

When to Use Each Pattern

  • join_all: Simple cases where you want all requests to complete
  • tokio::spawn: When you need true parallelism and independent task management
  • Semaphores: When you need to limit concurrency to respect rate limits
  • Streams: For processing large numbers of URLs with backpressure control

Concurrent requests with Reqwest significantly improve performance for I/O-bound operations. Choose the appropriate pattern based on your specific requirements for error handling, concurrency limits, and resource management.

Try WebScraping.AI for Your Web Scraping Needs

Looking for a powerful web scraping solution? WebScraping.AI provides an LLM-powered API that combines Chromium JavaScript rendering with rotating proxies for reliable data extraction.

Key Features:

  • AI-powered extraction: Ask questions about web pages or extract structured data fields
  • JavaScript rendering: Full Chromium browser support for dynamic content
  • Rotating proxies: Datacenter and residential proxies from multiple countries
  • Easy integration: Simple REST API with SDKs for Python, Ruby, PHP, and more
  • Reliable & scalable: Built for developers who need consistent results

Getting Started:

Get page content with AI analysis:

curl "https://api.webscraping.ai/ai/question?url=https://example.com&question=What is the main topic?&api_key=YOUR_API_KEY"

Extract structured data:

curl "https://api.webscraping.ai/ai/fields?url=https://example.com&fields[title]=Page title&fields[price]=Product price&api_key=YOUR_API_KEY"

Try in request builder

Related Questions

Get Started Now

WebScraping.AI provides rotating proxies, Chromium rendering and built-in HTML parser for web scraping
Icon