Skip to content

RetryPolicy and Backoff

Most code configures retries in the attribute. These are the types that attribute expands into, and what tests and hand-built QueueConfigs use directly.

pub struct RetryPolicy {
pub max_attempts: u32,
pub backoff: Backoff,
}
impl RetryPolicy {
pub fn new(max_attempts: u32, backoff: Backoff) -> Self;
pub fn none() -> Self;
pub fn exponential(max_attempts: u32) -> Self;
pub fn fixed(max_attempts: u32, delay: Duration) -> Self;
pub fn decide(&self, failed_attempt: u32) -> RetryDecision;
}
impl Default for RetryPolicy; // max_attempts: 1, Backoff::None

max_attempts counts the first try. 1 means no retries.

RetryPolicy::none() // 1 attempt, Backoff::None
RetryPolicy::exponential(5) // 5 attempts, Backoff::exponential()
RetryPolicy::fixed(3, Duration::from_millis(500)) // 3 attempts, constant 500ms
pub enum Backoff {
None,
Fixed(Duration),
Exponential { base: Duration, factor: f64, max: Duration, jitter: bool },
}
impl Backoff {
pub fn exponential() -> Self; // base 1s, factor 2.0, max 300s, jitter true
pub fn delay_for(&self, failed_attempt: u32) -> Duration;
}

Backoff::None retries immediately, with a delay of Duration::ZERO.

Exponential waits min(max, base * factor^(failed_attempt - 1)). With jitter on, the wait is drawn uniformly from [0, computed] — full jitter.

pub enum RetryDecision {
Retry { delay: Duration },
GiveUp,
}

decide(failed_attempt) returns GiveUp when failed_attempt >= max_attempts, and otherwise Retry with backoff.delay_for(failed_attempt).

RetryDecision is exported from the crate root but not from the prelude, since application code rarely names it.

Source max_attempts Backoff
RetryPolicy::default() and ::none() 1 Backoff::None
QueueConfig::new(...) 1 Backoff::None
Backoff::exponential() base 1s, factor 2.0, max 300s, jitter true
RetryPolicy::exponential(n) n Backoff::exponential()
RetryPolicy::fixed(n, d) n Backoff::Fixed(d)
The retry(...) attribute 3 exponential, base 1s, factor 2.0, max 5m, jitter true

The first two rows are the trap: a queue with no retry(...) does not retry at all.

  1. Job::retry_policy(), when it returns Some.
  2. Otherwise Job::QUEUE.config().retry.

Written to be total, on purpose — a backoff calculation is the last place you want a panic:

  • failed_attempt is clamped to at least 1, so attempt 0 behaves like 1;
  • the exponentiation happens in f64 and saturates to max on overflow;
  • a non-finite or non-positive factor degenerates to a constant base;
  • a non-finite or NaN intermediate result falls back to max;
  • a zero base or a zero max yields Duration::ZERO;
  • the result is always less than or equal to max.

RetryPolicy and Backoff are Serialize + Deserialize + Clone + PartialEq + Debug, so a policy can come from configuration rather than from an attribute when a deployment needs to tune it without a rebuild.

Retries and backoff, and queuey_core::retry on docs.rs.