What is the difference between reqwest::ClientBuilder::timeout and connect_timeout for controlling request timeouts?
timeout sets a deadline for the entire HTTP request lifecycle (connection establishment, request sending, and response receiving), while connect_timeout specifically limits how long the client waits for the initial TCP connection to succeed. These timeouts operate at different layers: connect_timeout governs the TCP handshake before any HTTP data is exchanged, and timeout is an end-to-end deadline that covers everything from DNS resolution through receiving the complete response body. Using them together provides defense in depth—connect_timeout prevents hanging on unreachable hosts, while timeout ensures the overall operation doesn't exceed a maximum duration regardless of where delays occur.
The Request Lifecycle
use reqwest::Client;
use std::time::Duration;
// A complete HTTP request has multiple phases:
//
// 1. DNS Resolution - Convert hostname to IP address
// 2. TCP Connection - Establish TCP socket (connect_timeout covers this)
// 3. TLS Handshake - HTTPS only, negotiate encryption
// 4. HTTP Request - Send request headers and body
// 5. Server Processing - Server generates response
// 6. HTTP Response - Receive response headers and body
//
// timeout covers: ALL of the above
// connect_timeout covers: Only step 2 (TCP Connection)
fn request_lifecycle() {
// Illustration:
//
// Timeline: |--DNS--|--TCP--|--TLS--|--Request--|--Server--|--Response--|
// ^ ^
// | |
// connect_timeout |
// timeout starts from request initiation
// and covers everything
}Understanding which phase each timeout covers helps diagnose where delays occur.
Basic timeout Usage
use reqwest::Client;
use std::time::Duration;
fn basic_timeout() -> Result<(), Box<dyn std::error::Error>> {
// timeout applies to the entire request
let client = Client::builder()
.timeout(Duration::from_secs(5)) // Total request time limit
.build()?;
// This request must complete within 5 seconds total
// Including: DNS, connection, TLS, request, response
let response = client
.get("https://httpbin.org/delay/2")
.send()?;
println!("Status: {}", response.status());
Ok(())
}
fn timeout_exceeded() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::builder()
.timeout(Duration::from_secs(1)) // Very short timeout
.build()?;
// httpbin.org/delay/2 waits 2 seconds before responding
// This will fail because total time > 1 second
let result = client
.get("https://httpbin.org/delay/2")
.send();
match result {
Err(e) => {
// Error: request timed out
println!("Timeout error: {}", e);
}
Ok(_) => println!("Unexpected success"),
}
Ok(())
}timeout is a cumulative deadline from request start to response completion.
Basic connect_timeout Usage
use reqwest::Client;
use std::time::Duration;
fn basic_connect_timeout() -> Result<(), Box<dyn std::error::Error>> {
// connect_timeout applies only to TCP connection establishment
let client = Client::builder()
.connect_timeout(Duration::from_secs(3)) // Connection phase only
.build()?;
// The connection must complete within 3 seconds
// After connection, there's no time limit (unless timeout is also set)
let response = client
.get("https://httpbin.org/get")
.send()?;
println!("Status: {}", response.status());
Ok(())
}
fn connect_timeout_exceeded() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::builder()
.connect_timeout(Duration::from_millis(1)) // Very short
.build()?;
// Connecting to an unreachable or slow host
// This might fail during connection establishment
let result = client
.get("http://10.255.255.1:8080/") // Non-routable IP
.send();
match result {
Err(e) => {
println!("Connection error: {}", e);
}
Ok(_) => println!("Connected"),
}
Ok(())
}connect_timeout only limits the time to establish a TCP connection.
Combining Both Timeouts
use reqwest::Client;
use std::time::Duration;
fn combined_timeouts() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::builder()
.connect_timeout(Duration::from_secs(5)) // Max 5s to connect
.timeout(Duration::from_secs(30)) // Max 30s total
.build()?;
// Now we have two layers of protection:
// 1. If the server is unreachable, connect_timeout fails fast
// 2. If the server is slow to respond, overall timeout limits total time
let response = client
.get("https://httpbin.org/delay/10")
.send()?;
println!("Status: {}", response.status());
Ok(())
}
fn why_combine() {
// Scenario 1: Host is unreachable
// - connect_timeout fires after 5s
// - timeout would also fire, but connect_timeout is faster
// - Clearer error: "connection timed out" vs "request timed out"
// Scenario 2: Host is reachable but slow
// - Connection succeeds within 5s
// - Request takes 40s total
// - timeout fires at 30s
// - Error: "request timed out"
// Scenario 3: Everything works
// - Connection: 1s
// - Request/response: 20s
// - Total: 21s < 30s, success
}Using both timeouts provides granular control and clearer error messages.
Per-Request Timeouts
use reqwest::Client;
use std::time::Duration;
fn per_request_timeout() -> Result<(), Box<dyn std::error::Error>> {
// Client has default timeouts
let client = Client::builder()
.connect_timeout(Duration::from_secs(10))
.timeout(Duration::from_secs(60))
.build()?;
// Override timeout for specific request
let fast_response = client
.get("https://httpbin.org/get")
.timeout(Duration::from_secs(5)) // Override client default
.send()?;
// Override connect_timeout for specific request
let patient_connect = client
.get("https://slow-server.example.com/")
.connect_timeout(Duration::from_secs(30)) // Longer for slow networks
.send()?;
// The RequestBuilder methods override client defaults
// Use .timeout(None) to disable timeout for a specific request
Ok(())
}Individual requests can override client-level timeout settings.
Timeout Behavior Details
use reqwest::Client;
use std::time::Duration;
fn timeout_behavior() {
// Key behaviors to understand:
// 1. timeout starts when send() is called
// - Includes DNS resolution time
// - Includes connection time
// - Includes TLS handshake time
// - Includes sending request body time
// - Includes receiving response headers AND body
// 2. connect_timeout starts during TCP connect
// - Only applies to TCP connection phase
// - NOT the TLS handshake (comes after)
// 3. If timeout < connect_timeout, timeout wins
let client = Client::builder()
.connect_timeout(Duration::from_secs(30))
.timeout(Duration::from_secs(5))
.build();
// If connection takes 10s, timeout fires at 5s
// connect_timeout never fires because timeout is tighter
// 4. Timeouts and response bodies
// - timeout applies to reading the response body too
// - If server sends data slowly, timeout can fire during body read
}
async fn streaming_response() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::builder()
.timeout(Duration::from_secs(10))
.build()?;
let response = client
.get("https://httpbin.org/stream-bytes/1000000")
.send()
.await?;
// Response headers received quickly, but body is streaming
// timeout still applies while reading body
let body = response.bytes().await?;
// If reading takes > 10s total, this will fail
Ok(())
}timeout covers the entire async operation, including body streaming.
When connect_timeout Fires vs timeout Fires
use reqwest::Client;
use std::time::Duration;
fn error_diagnosis() {
// Different timeout errors indicate different problems:
// connect_timeout error:
// - Server is down
// - Firewall blocking connection
// - Network unreachable
// - Server overloaded (can't accept connections fast enough)
// timeout error (after connection):
// - Server processing is slow
// - Response body is large
// - Network bandwidth is limited
// - Server is streaming slowly
// Knowing which timeout fired helps diagnose the issue
}
#[tokio::main]
async fn diagnose_timeout() {
let client = Client::builder()
.connect_timeout(Duration::from_secs(5))
.timeout(Duration::from_secs(30))
.build()
.unwrap();
match client.get("https://example.com/api").send().await {
Ok(response) => println!("Success: {}", response.status()),
Err(e) => {
// Check error type for timeout cause
if e.is_timeout() {
// Could be connect_timeout or overall timeout
// Error message usually indicates which one
if e.to_string().contains("connect") {
println!("Connection failed - server unreachable?");
} else {
println!("Request timed out - server too slow?");
}
} else if e.is_connect() {
println!("Connection error - network issue?");
} else {
println!("Other error: {}", e);
}
}
}
}Error messages help distinguish between connection and overall timeouts.
No Timeout (Infinite Wait)
use reqwest::Client;
use std::time::Duration;
fn no_timeout() -> Result<(), Box<dyn std::error::Error>> {
// By default, reqwest has a timeout of 30 seconds for the whole request
// (As of recent versions - check your version's defaults)
// Disable timeout entirely (wait forever)
let client = Client::builder()
.timeout(None) // No overall timeout
.build()?;
// This can hang indefinitely if server doesn't respond
// Use carefully - only when you have other ways to detect hangs
// Disable connect_timeout (wait forever for connection)
let client = Client::builder()
.connect_timeout(None)
.timeout(Duration::from_secs(60))
.build()?;
// This will wait indefinitely for TCP connection
// Then timeout after 60s total
Ok(())
}
fn when_no_timeout() {
// Use None carefully, typically for:
// - Long-polling endpoints
// - Server-sent events (SSE)
// - Very large file downloads with progress monitoring
// - WebSocket upgrades
// But consider using per-request timeout overrides instead
}Setting None disables timeouts, which can be useful but risky.
Real-World Configuration Patterns
use reqwest::Client;
use std::time::Duration;
fn production_client() -> Result<Client, reqwest::Error> {
// Typical production configuration
Client::builder()
.connect_timeout(Duration::from_secs(10)) // Allow slow DNS/network
.timeout(Duration::from_secs(60)) // Reasonable total time
.pool_idle_timeout(Duration::from_secs(90)) // Connection pooling
.pool_max_idle_per_host(10) // Connection reuse
.user_agent("my-app/1.0")
.build()
}
fn api_client() -> Result<Client, reqwest::Error> {
// Fast API calls - tighter timeouts
Client::builder()
.connect_timeout(Duration::from_secs(5))
.timeout(Duration::from_secs(15))
.build()
}
fn file_download_client() -> Result<Client, reqwest::Error> {
// Long downloads - loose timeout
Client::builder()
.connect_timeout(Duration::from_secs(10))
.timeout(Duration::from_secs(300)) // 5 minutes for large files
.build()
}
fn streaming_client() -> Result<Client, reqwest::Error> {
// SSE/WebSocket - long-lived connections
Client::builder()
.connect_timeout(Duration::from_secs(10))
.timeout(None) // Managed at application level
.build()
}Different use cases warrant different timeout configurations.
Interaction with Connection Pooling
use reqwest::Client;
use std::time::Duration;
fn connection_pooling() {
// reqwest uses a connection pool by default
// Pooled connections bypass connect_timeout on reuse
// Timeline for new connection:
// 1. Check pool - no matching connection
// 2. DNS resolution
// 3. TCP connect (connect_timeout applies)
// 4. TLS handshake
// 5. Add to pool for reuse
// Timeline for pooled connection:
// 1. Check pool - found matching connection
// 2. Reuse existing connection (no connect_timeout)
// 3. Send request
// timeout always applies, even to pooled connections
let client = Client::builder()
.connect_timeout(Duration::from_secs(5))
.timeout(Duration::from_secs(30))
.pool_idle_timeout(Duration::from_secs(60)) // Keep connections alive
.build();
// First request: connect_timeout + timeout apply
// Subsequent requests: only timeout applies (connection is reused)
}Connection pooling affects which timeouts apply to subsequent requests.
Testing Timeouts
use reqwest::Client;
use std::time::Duration;
#[tokio::test]
async fn test_connect_timeout() {
let client = Client::builder()
.connect_timeout(Duration::from_millis(100))
.build()
.unwrap();
// Non-routable IP address
let result = client
.get("http://10.255.255.1:12345/")
.send()
.await;
assert!(result.is_err());
assert!(result.unwrap_err().is_timeout() || result.unwrap_err().is_connect());
}
#[tokio::test]
async fn test_request_timeout() {
let client = Client::builder()
.timeout(Duration::from_millis(500))
.build()
.unwrap();
// httpbin delay endpoint
let result = client
.get("https://httpbin.org/delay/2") // Waits 2 seconds
.send()
.await;
assert!(result.is_err());
assert!(result.unwrap_err().is_timeout());
}
#[tokio::test]
async fn test_successful_request() {
let client = Client::builder()
.timeout(Duration::from_secs(10))
.build()
.unwrap();
let result = client
.get("https://httpbin.org/get")
.send()
.await;
assert!(result.is_ok());
}Testing timeout behavior requires endpoints that deliberately delay responses.
Handling Timeout Errors Gracefully
use reqwest::Client;
use std::time::Duration;
#[tokio::main]
async fn robust_request() -> Result<String, String> {
let client = Client::builder()
.connect_timeout(Duration::from_secs(5))
.timeout(Duration::from_secs(30))
.build()
.map_err(|e| format!("Client error: {}", e))?;
match client.get("https://api.example.com/data").send().await {
Ok(response) => {
if response.status().is_success() {
response
.text()
.await
.map_err(|e| format!("Body 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 - check network or server".to_string())
} else {
Err(format!("Request error: {}", e))
}
}
}
}
#[tokio::main]
async fn retry_on_timeout() -> Result<String, Box<dyn std::error::Error>> {
let client = Client::builder()
.timeout(Duration::from_secs(10))
.build()?;
let url = "https://api.example.com/data";
let mut retries = 3;
loop {
match client.get(url).send().await {
Ok(response) => {
return Ok(response.text().await?);
}
Err(e) if e.is_timeout() && retries > 0 => {
retries -= 1;
tokio::time::sleep(Duration::from_secs(1)).await;
continue;
}
Err(e) => return Err(e.into()),
}
}
}Graceful error handling distinguishes between timeout types and enables retry strategies.
Synthesis
Quick reference:
use reqwest::Client;
use std::time::Duration;
fn quick_reference() {
// connect_timeout: TCP connection establishment only
// - Limits time to complete TCP handshake
// - Doesn't cover TLS, request, or response
// - Use to fail fast on unreachable hosts
// timeout: Entire request lifecycle
// - Covers DNS, connection, TLS, request, response
// - Includes reading response body
// - Use to limit total operation time
// Typical configuration:
let client = Client::builder()
.connect_timeout(Duration::from_secs(5)) // Fast fail on bad hosts
.timeout(Duration::from_secs(30)) // Reasonable total limit
.build();
// Per-request override:
client.get("url")
.timeout(Duration::from_secs(60)) // Override for slow endpoint
.connect_timeout(Duration::from_secs(10)) // Override connection time
.send();
// Disable:
client.get("url")
.timeout(None) // No timeout (risky)
.connect_timeout(None) // No connection timeout (risky)
.send();
}Key insight: connect_timeout and timeout operate at different layers of the network stack, giving you fine-grained control over where failures can occur. connect_timeout is narrowly scoped to the TCP handshake—it's your defense against hanging on unresponsive or unreachable hosts. When the connection succeeds but the server is slow, connect_timeout doesn't help; that's where timeout applies, covering the entire request from DNS through response body. Setting both timeouts is a defensive pattern: use a relatively short connect_timeout (5-10 seconds typically) to fail fast on network issues, and a longer timeout (30-60 seconds or more) to handle slow but reachable servers. The error type when timeouts fire helps diagnose the root cause: connection failures suggest infrastructure issues (DNS, firewall, server down), while overall timeouts suggest application issues (slow processing, large responses, constrained bandwidth). For long-running operations like file downloads or streaming, consider disabling or significantly extending timeout while keeping connect_timeout to ensure you don't wait forever for an unreachable server.
