v0.3.0 — Retry and backoff
Pre-releasethrottle-net v0.3.0 — Retry and backoff
Resilience that stands on its own. v0.3.0 adds the retry half of the library: a complete backoff taxonomy with jitter, a retry policy that classifies errors and honors Retry-After, and a dependency-free Retry-After parser. None of it requires a limiter — retry any fallible async call — but it composes cleanly with one. No breaking changes.
What is throttle-net?
A general-purpose outbound throttling and resilience library. Where rate-net protects your service from being overwhelmed (inbound), throttle-net protects your service from overwhelming the downstreams it calls — and from being banned by them. It paces your outbound work, composes limits across dimensions and scopes, and — as of this release — retries transient failures without stampeding.
What's new in 0.3.0
Backoff — the delay taxonomy
A backoff is a policy that turns an attempt number into a delay. Three base curves — constant, linear, exponential — each combine with a jitter mode.
use std::time::Duration;
use throttle_net::{Backoff, Jitter};
let backoff = Backoff::exponential(Duration::from_millis(100), 2.0)
.with_max(Duration::from_secs(30))
.with_jitter(Jitter::Decorrelated);
let mut delays = backoff.iter();
let first = delays.next_delay();The jitter modes follow the AWS taxonomy:
- None — exactly the capped curve.
- Full — uniform in
[0, delay]; maximum spread. - Equal —
delay/2 + rand(0, delay/2); keeps a floor while spreading. - Decorrelated —
min(max, rand(base, previous*3)); self-correlated, the strongest at breaking up a thundering herd, and the [Backoff::default].
Randomness comes from a small, no-dependency SplitMix64 generator — jitter needs spread, not cryptography. iter_seeded(seed) produces reproducible sequences for tests. A BackoffIter yields one delay per attempt and is an infinite Iterator; bounding attempts is the retry policy's job.
Retry — the policy
Retry drives a fallible async operation with a backoff, an attempt ceiling, and per-error classification.
use throttle_net::{Backoff, Retry, RetryAction};
let retry = Retry::new(Backoff::default()).max_attempts(5);
let result = retry
.run(
|| async { call_downstream().await },
|err| if err.is_transient() { RetryAction::Retry } else { RetryAction::GiveUp },
)
.await;The classifier returns a RetryAction per error — Retry (use the backoff), RetryAfter(duration) (honor a server hint), or GiveUp — so retry works with any error type. For error-forge errors, retry_if_retryable classifies by the error's own is_retryable().
Retry-After — parsed and honored
parse_retry_after handles both header forms RFC 9110 allows: a delay in seconds and an HTTP date (all three date formats — IMF-fixdate, RFC 850, asctime). It needs no date-library dependency and never panics on malformed input — bad values return None. A classifier turns a parsed value into RetryAction::RetryAfter, which the policy honors over its computed backoff when respect_retry_after(true) is set.
use throttle_net::{parse_retry_after, RetryAction};
let action = match parse_retry_after("120") {
Some(after) => RetryAction::RetryAfter(after),
None => RetryAction::Retry,
};The runnable examples/retry_backoff.rs shows a flaky downstream retried with jittered backoff, switching to the server's Retry-After on a 429.
Breaking changes
None. Everything in this release is additive.
Verification
Run on Windows x86_64, Rust stable 1.93.1; the same commands run in the CI matrix on Linux, macOS, and Windows across stable and MSRV 1.85:
cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo clippy --all-targets --no-default-features -- -D warnings
cargo test --all-features
cargo test --no-default-features
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features
cargo bench --bench throttle_bench
cargo deny check
cargo auditAll green. Counts at this tag (--all-features): 75 unit tests, 7 property tests, 2 retry integration tests, 55 doctests. The decorrelated-jitter thundering-herd scatter and the Retry-After parse-and-honor path are covered by dedicated integration tests; retry timing is verified deterministically under tokio's paused clock.
What's next
- v0.4.0 — Resilience. Circuit breaker (closed / open / half-open, count-, ratio-, and rolling-window thresholds) that wraps any limiter and fails fast when open; an exact sliding-window log; and queue policies (bounded, deadline-aware, priority, fair-across-keys).
Installation
[dependencies]
throttle-net = "0.3"MSRV: Rust 1.85.
Documentation
Changelog: CHANGELOG.md.
Full Changelog: v0.2.0...v0.3.0