tokio-retry
Composable async retry strategies — exponential backoff, Fibonacci backoff, and fixed intervals — for any Tokio future.
Repository Health
Technical Analysis
tokio-retry is a small, focused Rust crate that wraps any fallible async action in a retry loop driven by a pluggable delay strategy. Instead of hand-rolling a loop with tokio::time::sleep calls, you hand Retry::start an iterator of Durations and an FnMut() -> Future action; the crate polls the action, and on failure advances the strategy iterator to compute the next delay before trying again. The strategy module ships ExponentialBackoff, FibonacciBackoff, and FixedInterval as plain Iterator<Item = Duration> implementations, plus an optional jitter() helper that randomizes a delay to avoid thundering-herd retries across many clients.
Because strategies are just iterators, they compose with the standard iterator adapters — .take(n) caps the retry count, .map(jitter) adds randomness, and custom strategies are trivial to write by implementing Iterator<Item = Duration> yourself. RetryIf extends the same mechanism with a Condition<Error> predicate so only certain error types trigger a retry (e.g. retry on a transient network error but not on an authentication failure). The crate is no_std compatible outside of its tokio::time dependency, has no required feature flags beyond rand for jitter, and is a common building block underneath HTTP clients, database reconnection logic, and any other operation that talks to a flaky external system over a Tokio runtime.
What You Get
Retry::start(strategy, action)— drives anFnMut() -> Futureaction through repeated attempts using anyIterator<Item = Duration>as the delay scheduleRetryIf::start(strategy, action, condition)— the same driver, but only retries when aCondition<Error>predicate returns true for the failure- Three ready-made backoff strategies —
ExponentialBackoff,FibonacciBackoff, andFixedInterval— each configurable with.factor()and.max_delay() - An optional
jitter()helper (behind the defaultrandfeature) that randomizes a computed delay to spread out simultaneous retries no_stdsupport outside of thetokio::timedependency, keeping the crate usable in constrained async runtimes
Common Use Cases
- Retrying a failed HTTP request to a flaky upstream API with exponential backoff and jitter
- Reconnecting to a database or message broker after a dropped connection using a fixed or Fibonacci interval
- Wrapping cloud SDK calls that can hit transient rate limits, retrying only on retryable error codes via
RetryIf - Polling an external job or task status endpoint with backoff until it reports completion or a hard failure
- Building higher-level resilience utilities (circuit breakers, request queues) on top of a well-tested backoff primitive instead of reimplementing one
Under The Hood
Architecture
tokio-retry’s core is a small pin-projected state machine in src/lib.rs: RetryState<A> is either Running { future: A::Future } or Sleeping { future: Sleep }, and RetryIf::poll alternates between the two — polling the in-flight action, and on Err, checking the Condition before advancing the strategy iterator, arming a tokio::time::sleep_until deadline, and switching state to Sleeping. Retry is a thin wrapper around RetryIf with an always-true condition, so the two entry points share one implementation. The strategy module is architecturally independent — each strategy is a plain Iterator<Item = Duration> with no knowledge of futures or polling at all, which is what lets .take(), .map(jitter), and custom iterators compose directly as retry schedules.
Tech Stack
The crate targets Rust edition 2018 with an MSRV of 1.85, depends on tokio (only the time feature, not the full runtime), pin-project-lite for the state machine’s pinned enum, and an optional rand dependency (enabled by default) gating the jitter() function. It builds no_std aside from the tokio::time import, so it carries almost no transitive dependency weight. CI runs cargo test across Ubuntu, macOS, and Windows on both stable and beta Rust, plus a dedicated MSRV check, cargo doc with --document-private-items, cargo-semver-checks for API compatibility, and a cargo-deny dependency audit.
Code Quality
Tests live in tests/future.rs as async integration tests (#[tokio::test]) covering single-attempt failure, retry-until-exhausted, retry-until-success, and conditional-retry paths, complemented by dense unit tests inside src/strategy.rs that exercise each backoff strategy’s saturation, max-delay clamping, and factor-scaling behavior. #[lints.rust] and #[lints.clippy] sections in Cargo.toml enable strict warnings (unreachable_pub, use_self, std_instead_of_core, upper_case_acronyms, manual_let_else, and more), and rustfmt/clippy gates run in CI across default, no-default, and all-features builds, so the small surface area is held to a high consistency bar.
API Design
The public surface is deliberately tiny: two constructors (Retry::start, RetryIf::start), three strategy types with a shared .factor()/.max_delay() builder pattern, and a single jitter() free function. Any FnMut() -> impl Future<Output = Result<T, E>> works as an action with zero boilerplate — there’s no trait to implement for the common case — and strategies being plain iterators means existing knowledge of Iterator adapters transfers directly instead of requiring a bespoke DSL. The deprecated spawn() aliases (kept for one release with a #[deprecated] note pointing to start()) show a considered approach to breaking API changes.