retryx
Small, async-first retry & backoff helper for Tokio-based Rust services.
retryx gives you a fluent builder API to retry any async operation with fixed or exponential backoff, optional per-attempt timeouts, custom retry filters, and an on_retry hook for logging or metrics.
Features
- Async-first: Designed for
async/await and tokio.
- Retry any async function: Works with
Fn() -> Future<Output = Result<T, E>>.
- Delay strategies: Fixed delay and exponential backoff with sane caps.
- Per-attempt timeout: Fail attempts that overrun, using
tokio::time::timeout.
- Custom retry filters: Decide which errors are retryable.
on_retry hook: Integrate logging and metrics.
- No macros, no unsafe: Simple, readable implementation.
Quick start
Add this to your Cargo.toml:
[dependencies]
retryx = "0.1"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
Example: basic retry with exponential backoff
use retryx::retry;
use std::time::Duration;
#[derive(Debug, Clone)]
struct ApiError;
impl std::fmt::Display for ApiError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "api error")
}
}
impl std::error::Error for ApiError {}
impl From<tokio::time::error::Elapsed> for ApiError {
fn from(_: tokio::time::error::Elapsed) -> Self {
ApiError
}
}
async fn call_api() -> Result<String, ApiError> {
Ok("ok".to_string())
}
#[tokio::main]
async fn main() -> Result<(), ApiError> {
let value = retry::<ApiError>()
.times(3)
.exponential()
.timeout(Duration::from_secs(2))
.run(|| async {
call_api().await
})
.await?;
println!("Got value: {value}");
Ok(())
}
Example: custom filter and on_retry hook
use retryx::retry;
use std::time::Duration;
#[derive(Debug, Clone)]
enum ApiError {
Timeout,
RateLimited,
Fatal,
}
impl std::fmt::Display for ApiError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ApiError::Timeout => write!(f, "timeout"),
ApiError::RateLimited => write!(f, "rate limited"),
ApiError::Fatal => write!(f, "fatal error"),
}
}
}
impl std::error::Error for ApiError {}
impl From<tokio::time::error::Elapsed> for ApiError {
fn from(_: tokio::time::error::Elapsed) -> Self {
ApiError::Timeout
}
}
async fn call_api() -> Result<String, ApiError> {
Err(ApiError::Timeout)
}
#[tokio::main]
async fn main() -> Result<(), ApiError> {
let result = retry::<ApiError>()
.times(5)
.fixed(Duration::from_millis(200))
.when(|err| matches!(err, ApiError::Timeout | ApiError::RateLimited))
.on_retry(|attempt, err, delay| {
println!("retry {attempt}: {err}, next in {delay:?}");
})
.run(|| async {
call_api().await
})
.await;
println!("Final result: {:?}", result);
Ok(())
}