What are the best practices for handling timeouts in Reqwest?
Proper timeout handling is crucial for building robust HTTP clients with Reqwest, Rust's popular HTTP library. Timeouts prevent your application from hanging indefinitely when network conditions are poor or servers are unresponsive. This guide covers comprehensive timeout strategies and best practices for production-ready applications.
Understanding Reqwest Timeout Types
Reqwest provides several timeout configurations to handle different scenarios:
Connection Timeout
The connection timeout controls how long to wait when establishing a connection to the server:
use reqwest::Client;
use std::time::Duration;
let client = Client::builder()
.connect_timeout(Duration::from_secs(10))
.build()?;
let response = client.get("https://api.example.com/data")
.send()
.await?;
Request Timeout
The request timeout sets a limit for the entire HTTP request lifecycle, including connection, sending data, and receiving the response:
let client = Client::builder()
.timeout(Duration::from_secs(30))
.build()?;
// This request will timeout after 30 seconds total
let response = client.get("https://slow-api.example.com/large-dataset")
.send()
.await?;
Per-Request Timeouts
You can override client-level timeouts for individual requests:
let client = Client::new();
let response = client.get("https://api.example.com/quick-data")
.timeout(Duration::from_secs(5))
.send()
.await?;
Implementing Robust Timeout Strategies
1. Layered Timeout Configuration
Use multiple timeout layers for comprehensive protection:
use reqwest::{Client, Error};
use std::time::Duration;
use tokio::time::timeout as tokio_timeout;
async fn robust_http_request(url: &str) -> Result<String, Box<dyn std::error::Error>> {
let client = Client::builder()
.connect_timeout(Duration::from_secs(10)) // Connection timeout
.timeout(Duration::from_secs(30)) // Request timeout
.build()?;
// Additional application-level timeout
let response = tokio_timeout(
Duration::from_secs(45),
client.get(url).send()
).await??;
let text = tokio_timeout(
Duration::from_secs(15),
response.text()
).await??;
Ok(text)
}
2. Timeout with Retry Logic
Combine timeouts with intelligent retry mechanisms:
use reqwest::{Client, Error, StatusCode};
use std::time::Duration;
use tokio::time::sleep;
struct RetryConfig {
max_attempts: u32,
base_delay: Duration,
max_delay: Duration,
timeout: Duration,
}
impl Default for RetryConfig {
fn default() -> Self {
Self {
max_attempts: 3,
base_delay: Duration::from_millis(500),
max_delay: Duration::from_secs(10),
timeout: Duration::from_secs(30),
}
}
}
async fn request_with_retry(
client: &Client,
url: &str,
config: &RetryConfig,
) -> Result<reqwest::Response, Error> {
let mut attempt = 0;
loop {
attempt += 1;
match client.get(url)
.timeout(config.timeout)
.send()
.await
{
Ok(response) => {
if response.status().is_success() {
return Ok(response);
} else if response.status() == StatusCode::TOO_MANY_REQUESTS {
// Handle rate limiting with exponential backoff
if attempt >= config.max_attempts {
return Ok(response);
}
} else if response.status().is_server_error() && attempt < config.max_attempts {
// Retry on server errors
} else {
return Ok(response);
}
}
Err(e) => {
if attempt >= config.max_attempts {
return Err(e);
}
if e.is_timeout() || e.is_connect() {
// Retry on timeout or connection errors
} else {
return Err(e);
}
}
}
// Exponential backoff
let delay = std::cmp::min(
config.base_delay * 2_u32.pow(attempt - 1),
config.max_delay,
);
sleep(delay).await;
}
}
3. Context-Aware Timeout Handling
Implement different timeout strategies based on request type and criticality:
use reqwest::Client;
use std::time::Duration;
pub struct ApiClient {
client: Client,
}
impl ApiClient {
pub fn new() -> Result<Self, reqwest::Error> {
let client = Client::builder()
.connect_timeout(Duration::from_secs(10))
.build()?;
Ok(Self { client })
}
// Critical real-time requests
pub async fn get_critical_data(&self, url: &str) -> Result<String, reqwest::Error> {
let response = self.client.get(url)
.timeout(Duration::from_secs(5))
.send()
.await?;
response.text().await
}
// Large file downloads
pub async fn download_file(&self, url: &str) -> Result<bytes::Bytes, reqwest::Error> {
let response = self.client.get(url)
.timeout(Duration::from_secs(300)) // 5 minutes
.send()
.await?;
response.bytes().await
}
// Background data synchronization
pub async fn sync_data(&self, url: &str) -> Result<String, reqwest::Error> {
let response = self.client.get(url)
.timeout(Duration::from_secs(60))
.send()
.await?;
response.text().await
}
}
Advanced Timeout Patterns
1. Progressive Timeout Strategy
Implement timeouts that adapt based on historical performance:
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
#[derive(Clone)]
pub struct AdaptiveClient {
client: Client,
response_times: Arc<Mutex<HashMap<String, Vec<Duration>>>>,
}
impl AdaptiveClient {
pub fn new() -> Result<Self, reqwest::Error> {
let client = Client::builder()
.connect_timeout(Duration::from_secs(10))
.build()?;
Ok(Self {
client,
response_times: Arc::new(Mutex::new(HashMap::new())),
})
}
pub async fn get_with_adaptive_timeout(&self, url: &str) -> Result<String, reqwest::Error> {
let timeout = self.calculate_timeout(url);
let start = Instant::now();
let result = self.client.get(url)
.timeout(timeout)
.send()
.await?
.text()
.await;
// Record response time for future requests
if result.is_ok() {
self.record_response_time(url.to_string(), start.elapsed());
}
result
}
fn calculate_timeout(&self, url: &str) -> Duration {
let response_times = self.response_times.lock().unwrap();
if let Some(times) = response_times.get(url) {
if !times.is_empty() {
let avg = times.iter().sum::<Duration>() / times.len() as u32;
// Set timeout to 3x average response time, with min/max bounds
return std::cmp::max(
std::cmp::min(avg * 3, Duration::from_secs(60)),
Duration::from_secs(5)
);
}
}
Duration::from_secs(30) // Default timeout
}
fn record_response_time(&self, url: String, duration: Duration) {
let mut response_times = self.response_times.lock().unwrap();
let times = response_times.entry(url).or_insert_with(Vec::new);
times.push(duration);
// Keep only recent measurements
if times.len() > 10 {
times.remove(0);
}
}
}
2. Circuit Breaker with Timeouts
Implement a circuit breaker pattern that considers timeout failures:
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
enum CircuitState {
Closed,
Open(Instant),
HalfOpen,
}
pub struct CircuitBreakerClient {
client: Client,
state: Arc<Mutex<CircuitState>>,
failure_threshold: u32,
failure_count: Arc<Mutex<u32>>,
reset_timeout: Duration,
}
impl CircuitBreakerClient {
pub fn new(failure_threshold: u32, reset_timeout: Duration) -> Result<Self, reqwest::Error> {
let client = Client::builder()
.connect_timeout(Duration::from_secs(10))
.timeout(Duration::from_secs(30))
.build()?;
Ok(Self {
client,
state: Arc::new(Mutex::new(CircuitState::Closed)),
failure_threshold,
failure_count: Arc::new(Mutex::new(0)),
reset_timeout,
})
}
pub async fn request(&self, url: &str) -> Result<String, Box<dyn std::error::Error>> {
// Check circuit state
{
let mut state = self.state.lock().unwrap();
match *state {
CircuitState::Open(opened_at) => {
if opened_at.elapsed() > self.reset_timeout {
*state = CircuitState::HalfOpen;
} else {
return Err("Circuit breaker is open".into());
}
}
_ => {}
}
}
match self.client.get(url).send().await {
Ok(response) => {
if response.status().is_success() {
self.on_success();
Ok(response.text().await?)
} else {
self.on_failure();
Err(format!("HTTP error: {}", response.status()).into())
}
}
Err(e) => {
if e.is_timeout() || e.is_connect() {
self.on_failure();
}
Err(Box::new(e))
}
}
}
fn on_success(&self) {
let mut failure_count = self.failure_count.lock().unwrap();
*failure_count = 0;
let mut state = self.state.lock().unwrap();
*state = CircuitState::Closed;
}
fn on_failure(&self) {
let mut failure_count = self.failure_count.lock().unwrap();
*failure_count += 1;
if *failure_count >= self.failure_threshold {
let mut state = self.state.lock().unwrap();
*state = CircuitState::Open(Instant::now());
}
}
}
Production Deployment Considerations
Environment-Specific Timeout Configuration
Configure timeouts based on deployment environment:
#[derive(Debug, Clone)]
pub struct TimeoutConfig {
pub connect_timeout: Duration,
pub request_timeout: Duration,
pub read_timeout: Duration,
}
impl TimeoutConfig {
pub fn for_environment(env: &str) -> Self {
match env {
"production" => Self {
connect_timeout: Duration::from_secs(15),
request_timeout: Duration::from_secs(45),
read_timeout: Duration::from_secs(30),
},
"staging" => Self {
connect_timeout: Duration::from_secs(10),
request_timeout: Duration::from_secs(30),
read_timeout: Duration::from_secs(20),
},
_ => Self { // development
connect_timeout: Duration::from_secs(5),
request_timeout: Duration::from_secs(15),
read_timeout: Duration::from_secs(10),
},
}
}
}
Monitoring and Alerting
Track timeout-related metrics for operational insights:
use prometheus::{Counter, Histogram, register_counter, register_histogram};
lazy_static::lazy_static! {
static ref REQUEST_TIMEOUTS: Counter = register_counter!(
"http_request_timeouts_total",
"Total number of HTTP request timeouts"
).unwrap();
static ref REQUEST_DURATION: Histogram = register_histogram!(
"http_request_duration_seconds",
"HTTP request duration in seconds"
).unwrap();
}
pub async fn monitored_request(client: &Client, url: &str) -> Result<String, reqwest::Error> {
let start = std::time::Instant::now();
let result = client.get(url)
.timeout(Duration::from_secs(30))
.send()
.await;
let duration = start.elapsed();
REQUEST_DURATION.observe(duration.as_secs_f64());
match result {
Ok(response) => response.text().await,
Err(e) => {
if e.is_timeout() {
REQUEST_TIMEOUTS.inc();
}
Err(e)
}
}
}
Integration with Web Scraping Workflows
When building web scraping applications, timeout handling becomes even more critical. Similar to how timeouts are handled in Puppeteer for browser automation, Reqwest timeouts should be configured based on the specific requirements of your scraping targets.
For complex scraping workflows that involve multiple HTTP requests, consider implementing timeout strategies that can handle the varying response times of different websites and API endpoints. When dealing with JavaScript-heavy websites, you might also need to complement Reqwest with tools that can handle dynamic content loading.
Common Timeout Configuration Patterns
API Scraping
For REST API scraping, use moderate timeouts with retry logic:
let api_client = Client::builder()
.connect_timeout(Duration::from_secs(5))
.timeout(Duration::from_secs(20))
.build()?;
Large File Downloads
For downloading large files, use extended timeouts:
let download_client = Client::builder()
.connect_timeout(Duration::from_secs(30))
.timeout(Duration::from_secs(600)) // 10 minutes
.build()?;
Real-time Data Feeds
For real-time or time-sensitive data, use short timeouts:
let realtime_client = Client::builder()
.connect_timeout(Duration::from_secs(2))
.timeout(Duration::from_secs(5))
.build()?;
Error Handling and Recovery
Implement comprehensive error handling for timeout scenarios:
use reqwest::Error;
async fn handle_timeout_errors(client: &Client, url: &str) -> Result<String, String> {
match client.get(url).send().await {
Ok(response) => {
if response.status().is_success() {
response.text().await.map_err(|e| format!("Response parsing error: {}", e))
} else {
Err(format!("HTTP error: {}", response.status()))
}
}
Err(e) => {
if e.is_timeout() {
Err("Request timed out - server may be overloaded".to_string())
} else if e.is_connect() {
Err("Connection failed - server may be unreachable".to_string())
} else {
Err(format!("Network error: {}", e))
}
}
}
}
Best Practices Summary
- Use Multiple Timeout Layers: Implement connection, request, and application-level timeouts
- Configure Context-Appropriate Timeouts: Set different timeouts based on request type and criticality
- Implement Retry Logic: Combine timeouts with exponential backoff retry mechanisms
- Monitor Timeout Metrics: Track timeout occurrences and response times for operational insights
- Use Circuit Breakers: Prevent cascading failures by implementing circuit breaker patterns
- Environment-Specific Configuration: Adjust timeout values based on deployment environment
- Handle Timeout Errors Gracefully: Provide meaningful error messages and fallback mechanisms
- Test Timeout Scenarios: Regularly test your application's behavior under timeout conditions
- Consider Adaptive Timeouts: Implement dynamic timeout adjustment based on historical performance
- Document Timeout Policies: Clearly document timeout configurations for maintenance teams
By following these best practices, you'll build robust HTTP clients with Reqwest that can handle network variability and provide reliable service to your applications. Proper timeout handling is essential for maintaining application performance and user experience in production environments.