What is the difference between reqwest::ClientBuilder::timeout and connect_timeout for request lifecycle control?

timeout sets a deadline for the entire request lifecycle—including DNS resolution, connection establishment, TLS negotiation, request transmission, and response reception—while connect_timeout specifically limits how long the client waits for a TCP connection to be established with the server. The key distinction is scope: timeout covers end-to-end request completion, whereas connect_timeout governs only the initial connection phase. A request can succeed within timeout but still fail connect_timeout if the connection takes too long, or fail timeout even after a fast connection if the response is slow.

The Request Lifecycle Phases

use reqwest::Client;
use std::time::Duration;
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Understanding the phases:
    // 1. DNS resolution - resolve hostname to IP address
    // 2. TCP connection - establish TCP socket with server
    // 3. TLS handshake - negotiate HTTPS (if applicable)
    // 4. Request transmission - send HTTP request
    // 5. Server processing - wait for server to generate response
    // 6. Response reception - receive HTTP response body
    
    // connect_timeout covers: TCP connection (phase 2)
    // timeout covers: ALL phases (1-6)
    
    Ok(())
}

Understanding which phase each timeout controls is essential for proper configuration.

Basic Timeout Configuration

use reqwest::Client;
use std::time::Duration;
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // timeout: Total request time
    let client = Client::builder()
        .timeout(Duration::from_secs(30))
        .build()?;
    
    // The entire request must complete within 30 seconds
    // Including: DNS, connection, TLS, request, response
    
    // connect_timeout: Only TCP connection phase
    let client = Client::builder()
        .connect_timeout(Duration::from_secs(5))
        .build()?;
    
    // The TCP connection must be established within 5 seconds
    // But the overall request can take longer
    
    // Both can be combined:
    let client = Client::builder()
        .connect_timeout(Duration::from_secs(5))
        .timeout(Duration::from_secs(30))
        .build()?;
    
    // Connection: max 5 seconds
    // Total request: max 30 seconds
    
    Ok(())
}

Configure both timeouts for comprehensive control over request behavior.

When Each Timeout Matters

use reqwest::Client;
use std::time::Duration;
 
#[tokio::main]
async fn main() {
    // Scenario 1: Server is slow to respond
    // - connect_timeout won't trigger (connection is fast)
    // - timeout WILL trigger (response is slow)
    
    // Scenario 2: Server is unreachable (network issues)
    // - connect_timeout WILL trigger (can't establish connection)
    // - timeout would also trigger, but connect_timeout fires first
    
    // Scenario 3: DNS resolution hangs
    // - Neither timeout covers DNS directly
    // - But timeout indirectly covers it (total request time)
    
    // Scenario 4: TLS handshake is slow
    // - connect_timeout won't cover TLS (only TCP)
    // - timeout covers the entire TLS negotiation
    
    // Scenario 5: Slow response body streaming
    // - connect_timeout passed during connection
    // - timeout covers slow streaming
}

Different failure modes require different timeout strategies.

Timeout Triggering Behavior

use reqwest::Client;
use std::time::Duration;
 
#[tokio::main]
async fn main() {
    let client = Client::builder()
        .connect_timeout(Duration::from_secs(2))
        .timeout(Duration::from_secs(5))
        .build()
        .unwrap();
    
    // Case 1: Connection takes 3 seconds
    // connect_timeout fires at 2 seconds
    // Error: "error sending request for url ... connect timeout"
    
    // Case 2: Connection takes 1 second, response takes 6 seconds
    // connect_timeout passes (1s < 2s)
    // timeout fires at 5 seconds total
    // Error: "error sending request for url ... operation timed out"
    
    // Case 3: Connection takes 1 second, response takes 3 seconds
    // Both timeouts pass
    // Success!
}

The timeout that fires depends on which phase is slow.

Connection Pooling Considerations

use reqwest::Client;
use std::time::Duration;
 
#[tokio::main]
async fn main() {
    // reqwest maintains a connection pool
    // Pooled connections skip the connect_timeout check
    
    let client = Client::builder()
        .connect_timeout(Duration::from_secs(5))
        .pool_max_idle_per_host(10)  // Keep connections alive
        .pool_idle_timeout(Duration::from_secs(60))
        .build()
        .unwrap();
    
    // First request: New connection -> connect_timeout applies
    // Subsequent requests: Pooled connection -> connect_timeout skipped
    // timeout ALWAYS applies, regardless of pooling
    
    // This means:
    // - First request to slow server: may fail connect_timeout
    // - Second request (reused connection): skips connect_timeout
    // - But timeout still limits total request time
}

Pooled connections bypass connect_timeout but not timeout.

Handling Timeout Errors

use reqwest::Client;
use std::time::Duration;
 
#[tokio::main]
async fn main() {
    let client = Client::builder()
        .connect_timeout(Duration::from_secs(2))
        .timeout(Duration::from_secs(5))
        .build()
        .unwrap();
    
    let result = client.get("https://httpbin.org/delay/10")
        .send()
        .await;
    
    match result {
        Ok(response) => {
            println!("Success: {}", response.status());
        }
        Err(e) => {
            if e.is_timeout() {
                if e.to_string().contains("connect") {
                    println!("Connection timeout - server unreachable or slow");
                } else {
                    println!("Request timeout - response took too long");
                }
            } else if e.is_connect() {
                println!("Connection failed - network or DNS issue");
            } else {
                println!("Other error: {}", e);
            }
        }
    }
}

Error messages distinguish between connection timeouts and request timeouts.

Default Behavior Without Timeouts

use reqwest::Client;
 
#[tokio::main]
async fn main() {
    // Without any timeout configuration:
    let client = Client::new();
    
    // connect_timeout: Uses system default (usually ~60-120 seconds)
    // timeout: No limit - request can hang indefinitely
    
    // This is problematic for:
    // - Long-running operations (can hang forever)
    // - Production services (need bounded request times)
    // - Resource management (open connections consume memory)
    
    // Recommendation: Always set at least timeout
}

Without explicit timeouts, requests can hang indefinitely.

Read Timeout vs Request Timeout

use reqwest::Client;
use std::time::Duration;
 
#[tokio::main]
async fn main() {
    // reqwest doesn't have separate read_timeout
    // timeout covers: connection + read + write
    
    // For more granular control, you might want:
    // - connect_timeout: How long to wait for TCP connection
    // - read_timeout: How long to wait for data on established connection
    // - write_timeout: How long to wait to send data
    
    // reqwest's timeout combines these
    // If you need read_timeout specifically, use the lower-level
    // hyper or tower services
    
    let client = Client::builder()
        .connect_timeout(Duration::from_secs(5))
        // .read_timeout(...) // Not available in reqwest
        .timeout(Duration::from_secs(30))
        .build()
        .unwrap();
}

reqwest uses a single timeout for the entire request; connect_timeout is the only granular option.

Per-Request Timeout Override

use reqwest::Client;
use std::time::Duration;
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::builder()
        .timeout(Duration::from_secs(30))  // Client default
        .build()?;
    
    // Override timeout for specific request
    let response = client
        .get("https://example.com/large-file")
        .timeout(Duration::from_secs(300))  // 5 minutes for large file
        .send()
        .await?;
    
    // This request uses 300s timeout
    // Other requests still use 30s default
    
    // Note: connect_timeout is client-level only
    // Cannot be overridden per-request
    
    Ok(())
}

Per-request timeouts override client defaults for specific use cases.

Streaming Response Bodies

use reqwest::Client;
use std::time::Duration;
use tokio_stream::StreamExt;
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::builder()
        .timeout(Duration::from_secs(5))
        .build()?;
    
    // When streaming response body:
    // timeout covers: request + response headers + initial body
    // It may or may not cover slow body streaming
    
    let response = client
        .get("https://example.com/streaming-endpoint")
        .send()
        .await?;
    
    // timeout covers getting to this point
    // Streaming the body afterward may not be covered
    
    // For streaming, consider:
    let mut stream = response.bytes_stream();
    while let Some(bytes) = stream.next().await {
        let bytes = bytes?;
        // Process bytes
        // This might not be covered by the original timeout
    }
    
    Ok(())
}

timeout applies to the request/response; streaming body may need separate handling.

Practical Timeout Values

use reqwest::Client;
use std::time::Duration;
 
fn create_client_for_api() -> Result<Client, reqwest::Error> {
    // For typical API calls
    Client::builder()
        .connect_timeout(Duration::from_secs(5))  // Network issues detected quickly
        .timeout(Duration::from_secs(30))         // Reasonable for API responses
        .build()
}
 
fn create_client_for_file_uploads() -> Result<Client, reqwest::Error> {
    // For file uploads
    Client::builder()
        .connect_timeout(Duration::from_secs(10)) // Same or slightly longer
        .timeout(Duration::from_secs(300))         // 5 minutes for large files
        .build()
}
 
fn create_client_for_health_checks() -> Result<Client, reqwest::Error> {
    // For health checks (should be fast)
    Client::builder()
        .connect_timeout(Duration::from_secs(2))   // Quick failure detection
        .timeout(Duration::from_secs(5))            // Health checks should be fast
        .build()
}
 
fn create_client_for_background_jobs() -> Result<Client, reqwest::Error> {
    // For background processing
    Client::builder()
        .connect_timeout(Duration::from_secs(10))
        .timeout(Duration::from_secs(600))         // 10 minutes
        .build()
}

Choose timeout values based on expected request characteristics.

Timeout and Retry Logic

use reqwest::Client;
use std::time::Duration;
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::builder()
        .connect_timeout(Duration::from_secs(5))
        .timeout(Duration::from_secs(30))
        .build()?;
    
    let url = "https://example.com/api";
    
    // Manual retry with timeout awareness
    let mut attempts = 0;
    let max_attempts = 3;
    
    loop {
        attempts += 1;
        
        match client.get(url).send().await {
            Ok(response) => {
                // Success - process response
                break Ok(response);
            }
            Err(e) if e.is_timeout() && attempts < max_attempts => {
                // Timeout occurred - retry
                println!("Timeout on attempt {}, retrying...", attempts);
                tokio::time::sleep(Duration::from_secs(1)).await;
                continue;
            }
            Err(e) if e.is_connect() && attempts < max_attempts => {
                // Connection issue - retry
                println!("Connection error on attempt {}, retrying...", attempts);
                tokio::time::sleep(Duration::from_secs(2)).await;
                continue;
            }
            Err(e) => {
                // Final failure
                break Err(e.into());
            }
        }
    }
}

Distinguishing timeout types helps implement appropriate retry strategies.

Comparison Table

// Summary comparison:
 
// connect_timeout:
// - Scope: TCP connection establishment only
// - Phase: After DNS, before TLS
// - Pooled connections: Skipped for reused connections
// - Per-request override: Not available
// - Typical value: 5-10 seconds
 
// timeout:
// - Scope: Entire request lifecycle
// - Phase: DNS + connection + TLS + request + response
// - Pooled connections: Always applies
// - Per-request override: Available via .timeout()
// - Typical value: 30-300 seconds (depends on use case)
 
// Key insight: connect_timeout is for network issues
//             timeout is for overall request duration

Debugging Timeout Issues

use reqwest::Client;
use std::time::Duration;
use tracing::{info, error};
 
#[tokio::main]
async fn main() {
    tracing_subscriber::fmt::init();
    
    let client = Client::builder()
        .connect_timeout(Duration::from_secs(5))
        .timeout(Duration::from_secs(30))
        .build()
        .unwrap();
    
    let start = std::time::Instant::now();
    
    match client.get("https://example.com").send().await {
        Ok(response) => {
            info!(
                elapsed_ms = start.elapsed().as_millis(),
                status = ?response.status(),
                "Request completed"
            );
        }
        Err(e) => {
            error!(
                elapsed_ms = start.elapsed().as_millis(),
                error = ?e,
                is_timeout = e.is_timeout(),
                is_connect = e.is_connect(),
                "Request failed"
            );
        }
    }
}

Timing information helps identify which phase is slow.

Synthesis

Timeout Scope Comparison:

Phase connect_timeout timeout
DNS resolution No Yes
TCP connection Yes Yes
TLS handshake No Yes
Request transmission No Yes
Server processing No Yes
Response reception No Yes

When to use each:

  • connect_timeout: Detect unreachable servers quickly; don't wait for TCP timeouts
  • timeout: Bound total request time; prevent indefinite hangs

Configuration recommendations:

Use Case connect_timeout timeout
API calls 5s 30s
File uploads 10s 300s
Health checks 2s 5s
Background jobs 10s 600s

Key insight: connect_timeout and timeout serve different purposes. connect_timeout protects against network connectivity issues—the time to establish a TCP socket. timeout protects against slow responses—the total time for the entire request. Use both: a short connect_timeout (5-10s) for fast failure detection on unreachable servers, and an appropriate timeout (varies by use case) for overall request duration bounds. The connect_timeout fires first if the server is unreachable; timeout fires first if the connection succeeds but the response is slow. For pooled connections, connect_timeout is skipped but timeout always applies.