Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Retry with Backoff

aioduct supports automatic retries with configurable exponential backoff. Retries can be set at the client level (applied to all requests) or per-request.

Basic Usage

use std::time::Duration;
use aioduct::{TokioClient, RetryConfig};

#[tokio::main]
async fn main() -> Result<(), aioduct::Error> {
    let client = TokioClient::new();

    let resp = client
        .get("http://example.com/api")?
        .retry(RetryConfig::default())
        .send()
        .await?;

    println!("status: {}", resp.status());
    Ok(())
}

RetryConfig

FieldTypeDefaultDescription
max_retriesu323Maximum number of retry attempts
initial_backoffDuration100msDelay before the first retry
max_backoffDuration30sUpper bound on backoff delay
backoff_multiplierf642.0Multiplier applied to backoff each attempt
retry_on_statusbooltrueWhether to retry on 5xx server errors
budgetOption<RetryBudget>NoneToken-bucket budget to prevent retry storms

The delay for attempt n (0-indexed) is:

delay = min(initial_backoff * multiplier^n, max_backoff)

What Gets Retried

By default, aioduct retries on:

  • Connection errors — I/O errors, hyper transport errors
  • Timeouts — request-level or client-level timeout exceeded
  • 5xx server errors — 500, 502, 503, etc. (when retry_on_status is true)
  • 429 Too Many Requests — rate limiting responses (when retry_on_status is true)

Client errors (4xx, other than 429) are never retried. To disable status-based retry, set retry_on_status(false).

Retry-After Header

When a server responds with a Retry-After header (common on 429 and 503 responses), aioduct uses the server’s requested delay instead of its own exponential backoff for that attempt. Both formats are supported:

  • Seconds: Retry-After: 120 — wait 120 seconds
  • HTTP-date: Retry-After: Wed, 21 Oct 2026 07:28:00 GMT — wait until the specified time

If the Retry-After value is missing or unparseable, the normal backoff delay is used.

Retry Budget

A RetryBudget prevents retry storms by limiting the total retry rate across all requests. Each successful (non-retried) request deposits tokens; each retry attempt withdraws one. When the budget is exhausted, retries are suppressed.

#![allow(unused)]
fn main() {
use std::time::Duration;
use aioduct::{TokioClient, RetryConfig, RetryBudget};

let client = TokioClient::builder()
    .retry(
        RetryConfig::default()
            .budget(RetryBudget::new(10, 1)),  // max 10 tokens, +1 per success
    )
    .build()?;
}

Client-Level Retry

Set a default retry policy for all requests:

#![allow(unused)]
fn main() {
use std::time::Duration;
use aioduct::{TokioClient, RetryConfig};

let client = TokioClient::builder()
    .retry(
        RetryConfig::default()
            .max_retries(5)
            .initial_backoff(Duration::from_millis(200))
            .max_backoff(Duration::from_secs(10)),
    )
    .build()?;
}

Per-Request Override

A retry config on a request takes precedence over the client default:

#![allow(unused)]
fn main() {
use std::time::Duration;
use aioduct::{TokioClient, RetryConfig};
let client = TokioClient::new();
let resp = client
    .post("http://example.com/idempotent-endpoint")?
    .retry(RetryConfig::default().max_retries(1))
    .body("payload")
    .send()
    .await?;
Ok::<_, aioduct::Error>(())
}

Example: Resilient LLM API Client

use std::time::Duration;
use aioduct::{TokioClient, RetryConfig};

#[tokio::main]
async fn main() -> Result<(), aioduct::Error> {
    let client = TokioClient::builder()
        .retry(
            RetryConfig::default()
                .max_retries(3)
                .initial_backoff(Duration::from_millis(500))
                .backoff_multiplier(2.0),
        )
        .timeout(Duration::from_secs(30))
        .build()?;

    let resp = client
        .post("https://api.example.com/v1/chat/completions")?
        .bearer_auth("sk-...")
        .header_str("content-type", "application/json")?
        .body(r#"{"model":"gpt-4","messages":[{"role":"user","content":"Hi"}]}"#)
        .send()
        .await?;

    println!("{}", resp.text().await?);
    Ok(())
}