Retries, Timeouts & Backoff
Make outbound HTTP calls resilient with timeouts, exponential backoff, jitter, and idempotency awareness.
Search across all documentation pages
Make outbound HTTP calls resilient with timeouts, exponential backoff, jitter, and idempotency awareness.
Quick-reference recipe card - copy-paste ready.
async fn with_retry<F, Fut, T, E>(mut f: F, max: u32) -> Result<T, E>
where
F: FnMut() -> Fut,
Fut: std::future::Future<Output = Result<T, E>>,
{
let mut attempt = 0;
loop {
match f().await {
Ok(v) => return Ok(v),
Err(e) if attempt >= max => return Err(e),
Err(_) => {
let delay = std::time::Duration::from_millis(100 * 2u64.pow(attempt));
tokio::time::sleep(delay).await;
attempt += 1;
}
}
}
}When to reach for this: Any call over the internet that can fail transiently - 502, connection reset, timeout.
use reqwest::StatusCode;
use std::time::Duration;
async fn fetch_with_policy(client: &reqwest::Client, url: &str) -> Result<String, reqwest::Error> {
let mut attempt = 0u32;
loop {
let result = client.get(url).send().await;
match result {
Ok(resp) if resp.status().is_success() => return resp.text().await,
Ok(resp) if resp.status() == StatusCode::TOO_MANY_REQUESTS => {
let retry_after = resp.headers()
.get("retry-after")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(2u64.pow(attempt.min(5)));
tokio::time::sleep(Duration::from_secs(retry_after)).await;
}
Ok(_) | Err(_) if attempt < 3 => {
let base = 100u64 * 2u64.pow(attempt);
let jitter = rand::random::<u64>() % 50;
tokio::time::sleep(Duration::from_millis(base + jitter)).await;
}
Ok(resp) => return resp.error_for_status().map(|_| unreachable!()),
Err(e) if e.is_timeout() && attempt < 3 => {}
Err(e) => return Err(e),
}
attempt += 1;
}
}What this demonstrates:
Retry-After on 429 responses.| Condition | Retry? |
|---|---|
| Connect timeout | Yes with backoff |
| 500/502/503/504 | Yes limited |
| 400/401/404 | No |
| POST without idempotency | No |
// reqwest-middleware + reqwest-retry crates for production policies| Alternative | Use When | Don't Use When |
|---|---|---|
Circuit breaker (tower) | Sustained outage | Rare blips only |
| Queue + worker | Absorb spikes async | Need synchronous response |
| Fallback cache | Degraded read mode | Strong consistency required |
3-5 for idempotent reads; 0-1 for writes unless idempotency guaranteed.
Connect 2-5s, total 10-30s depending on SLA.
tonic client middleware or service mesh policy.
Warn on retry with attempt count and upstream host.
Send duplicate request after delay - only for safe read paths.
Limit concurrent outbound calls per dependency.
wiremock sequence returning 503 then 200.
Retry with backoff; cache DNS at OS level.
Stop retries when local budget exhausted.
Metric upstream_retries_total{dependency}.
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.
Reviewed by Chris St. John·Last updated Jul 16, 2026