Open-source project
Xuanwo/backon avatar
Xuanwo/backon

BackON: retry as a method on the future, with a when predicate and a notify hook

Make retry like a built-in feature provided by Rust.

1,064 stars57 forksRustApache-2.0

At a glance

What is it?
BackON is a Rust crate that attaches retry behaviour to a function through a builder chain, so your_fn.retry(ExponentialBuilder::default()).await reads like standard library syntax. It covers blocking and async calls, custom backoff strategies including Retry-After, and it compiles for wasm and no_std, with a companion macro crate for shorter call sites.
Who is it for?
Adopt BackON if you want retry logic in Rust without hand-rolling a loop, a backoff calculation and a sleep, since the builder chain keeps the policy next to the call and separates the when predicate from the notify hook. Do not adopt it if you need a circuit breaker, a bulkhead, or jitter beyond what the built-in strategies give you, because the crate is scoped to retry and backoff only.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 127 days ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Retry as a builder on the future

BackON's stated goal is to make retry like a built-in feature provided by Rust, and the API shape is where that claim is tested. Instead of a wrapper function that takes a closure and a policy, retry is attached to the function itself as a method.

The README summarises the API as a native feel, with the example given as your_fn.retry(ExponentialBuilder::default()).await. The future is what you build the chain on, and the chain is the policy.

The traits are separate for the two execution models. Async code uses the Retryable trait, imported as use backon::Retryable, and blocking code uses BlockingRetryable, imported as use backon::BlockingRetryable. The blocking variant ends with .call()? rather than .await, and everything else in the chain is the same.

rust
use anyhow::Result;
use backon::ExponentialBuilder;
use backon::Retryable;

async fn fetch() -> Result<String> {
    Ok("hello, world!".to_string())
}

#[tokio::main]
async fn main() -> Result<()> {
    let content = fetch
        .retry(ExponentialBuilder::default())
        .sleep(tokio::time::sleep)
        .when(|e| e.to_string() == "EOF")
        .notify(|err: &anyhow::Error, dur: Duration| {
            println!("retrying {:?} after {:?}", err, dur);
        })
        .await?;
    println!("fetch succeeded: {}", content);

    Ok(())
}

The interesting part is the separation between the two hooks. when decides whether a given error is retryable, and notify fires when a retry is about to happen and receives the error and the computed duration. That separation lets you retry on a transient error while logging every attempt, without conflating the two decisions.

The sleep step, and why it is not optional

The quick start includes a step that looks like boilerplate and is not. The comment in the README reads that the sleep implementation is required if no feature has been enabled.

In the async example that step is .sleep(tokio::time::sleep), and in the blocking example it is .sleep(std::thread::sleep). The same line appears in both, with the sleep function swapped for the runtime.

This design is what makes the crate portable across runtimes and across no_std. The crate does not depend on tokio, or on any particular async runtime, and it does not pull in a timer of its own. Instead it asks you to name the sleep function, and it calls that one between attempts. If you enable the feature that matches your runtime, the step is filled in for you, and if you have not, you supply it.

The cost of that flexibility is one more line in the chain that you cannot forget, and a runtime mismatch that only shows up when the crate tries to wait. It is a reasonable trade for a library that also claims to work on wasm and no_std, where there is no standard timer at all.

The blocking example is otherwise identical to the async one, differing in the trait, the sleep function and the terminal call.

rust
use anyhow::Result;
use backon::BlockingRetryable;
use backon::ExponentialBuilder;

fn main() -> Result<()> {
    let content = fetch
        .retry(ExponentialBuilder::default())
        .sleep(std::thread::sleep)
        .when(|e| e.to_string() == "EOF")
        .call()?;
    Ok(())
}

Backoff strategies, including dynamic ones

The README lists built-in backoff strategies, exponential and constant, and says you can also define custom ones. That covers the common cases, but the more interesting capability is dynamic backoff, which the README gives as an example of using the HTTP Retry-After header.

A dynamic backoff matters when the server knows better than you do. A hard-coded exponential curve ignores a server that has told you it will be ready in thirty seconds, so you either wait too long or retry too early and get another error. A strategy that reads Retry-After turns a cooperative server into something the client can respect.

The builder is where the numeric policy lives, so the same chain can carry the numeric policy without changing the shape of the call site. The default is ExponentialBuilder::default(), which suggests the crate's opinionated default is exponential rather than constant.

The list of features in the README adds one more point that is easy to miss. Precise control is described as defining when to retry and getting notified via when and notify, which is the same pair the quick start shows. So the crate's answer to the hard part of retry, deciding which failures are transient, is left to you rather than baked in.

Workspace layout, macro crate and platform reach

The repository is a two member Cargo workspace.

toml
[workspace]
members = ["backon", "backon-macros"]
resolver = "2"

[workspace.package]
edition = "2024"
license = "Apache-2.0"
repository = "https://github.com/Xuanwo/backon"

The second member is a proc macro crate, published separately as backon-macros v1.0.0 on the same day as the v1.6.0 release of the main crate, 2025-10-18. A macro in this position is a convenience layer: it would let you express the same builder chain with less typing, at the cost of a second dependency and a second version to track.

The edition is 2024, which is worth noting because it means the crate requires a recent toolchain, and there is a rust-toolchain.toml in the tree that pins the expected version for contributors.

On platforms, the README says BackON works everywhere Rust does, including wasm and no_std. That claim is credible given the sleep design described earlier, since a crate that never assumes a runtime or a timer can be built for an environment that has neither. The no_std support also implies the crate avoids the standard library's clock for its own timing, which is the usual obstacle in that direction.

The repository also carries AGENTS.md, a .taplo.toml for TOML formatting, a rustfmt.toml, a CONTRIBUTING.md, and a Discord server badge in the README. The README points at the docs.rs examples index for more usage patterns, and asks people to submit issues for bug reports and questions via the discussions board.

Where BackON is the wrong tool

The scope is deliberately narrow, and the gaps are the usual ones for a retry library.

There is no circuit breaker. A retry loop that keeps trying against a service that is down will keep trying, and BackON does not provide the trip-and-hold-open behaviour you need to stop hammering a failing dependency. If your failure mode is a dependency that is fully down rather than one that is intermittently slow, you need that logic somewhere, and this crate is not where it lives.

There is no bulkhead or concurrency limit either. BackON controls how often one call is retried and how long it waits, not how many calls are in flight, so it does nothing for a fan-out that overwhelms a struggling service.

The when predicate is a closure over the error, and the example in the README matches on the error text, comparing e.to_string() against "EOF". That is a convenient demo and a poor production pattern, since matching on rendered error strings breaks the moment the message changes. The crate does not constrain how you write the predicate, so the quality of the retry decision is entirely on the caller.

On release cadence, the recent versions are v1.6.0 from 2025-10-18, backon-macros v1.0.0 from the same day, and v1.5.2 from 2025-07-30. The last push was on 2026-06-19. The repository is not archived, and a major version has been reached, but the gap between the last release and the last commit is worth knowing if you plan to rely on new features landing.

The licence is Apache-2.0, which is permissive and lets you use the crate in a proprietary product without publishing your own code, provided the licence and notice files travel with it.

Editorial conclusion

Adopt BackON if you want retry logic in Rust without hand-rolling a loop, a backoff calculation and a sleep, since the builder chain keeps the policy next to the call and separates the when predicate from the notify hook. Do not adopt it if you need a circuit breaker, a bulkhead, or jitter beyond what the built-in strategies give you, because the crate is scoped to retry and backoff only. Verify first that you have a sleep implementation wired in, since the README states the sleep step is required when no feature has been enabled, and note that the last push was on 2026-06-19 while the latest crate release is v1.6.0 from 2025-10-18.

Frequently asked questions

How do I retry an async function with BackON?

Import the Retryable trait, then attach the builder chain to the function: fetch.retry(ExponentialBuilder::default()).sleep(tokio::time::sleep).when(...).notify(...).await. The sleep step is required if no feature has been enabled, and the blocking equivalent uses BlockingRetryable and ends with .call().

What is the difference between the when and notify hooks in BackON?

when decides whether a given error should be retried, and notify is called when a retry is about to happen, receiving the error and the duration the crate will wait. The README describes this as precise control over when to retry and being notified.

Does BackON support custom or dynamic backoff?

Yes. The README lists built-in exponential and constant strategies, says you can define custom ones, and gives dynamic backoff as an example, such as using the HTTP Retry-After header to decide the wait.

Can BackON be used without the standard library?

The README states that BackON works everywhere Rust does, including wasm and no_std. The crate expects you to supply the sleep function, which is what keeps it independent of any particular async runtime or timer.

Official sources

  1. License: Apache-2.0
  2. Project website
  3. README
  4. Releases
  5. Xuanwo/backon on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/xuanwo-backon.svg)](https://hysenlabs.com/projects/xuanwo-backon)

Was this useful?