Concepts

Decisions

Allowed, Rejected, and Suppressed.

All admission checks return a RateLimitDecision:

  • Allowed: admitted and recorded
  • Rejected { window_size_seconds, retry_after_ms, remaining_after_waiting }: denied and not recorded, with backoff hints
  • Suppressed { suppression_factor, is_allowed }: suppressed strategy only; you must gate on is_allowed

Definition (from the crate):

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, strum_macros::Display)]
pub enum RateLimitDecision {
    /// Request is allowed to proceed.
    ///
    /// The increment has been recorded in the limiter's state.
    Allowed,

    /// Request exceeds the rate limit and is rejected.
    ///
    /// The increment was **not** recorded. Includes metadata for backoff.
    Rejected {
        /// Sliding window size used for this decision (in seconds).
        window_size_seconds: u64,

        /// Estimate of milliseconds until capacity becomes available.
        retry_after_ms: u64,

        /// Estimate of window usage after waiting `retry_after_ms`.
        remaining_after_waiting: u64,
    },

    /// Request handled by probabilistic suppression (suppressed strategy only).
    ///
    /// **Always check `is_allowed`** to determine if this specific call was admitted.
    Suppressed {
        /// Current suppression rate (0.0 = no suppression, 1.0 = full suppression).
        suppression_factor: f64,

        /// Whether this specific call was admitted.
        is_allowed: bool,
    },
}
If you use the suppressed strategy, always treat is_allowed as the admission signal.
use trypema::RateLimitDecision;

let decision: RateLimitDecision = decision;

match decision {
    RateLimitDecision::Allowed => {
        // Proceed
    }
    RateLimitDecision::Rejected {
        window_size_seconds,
        retry_after_ms,
        remaining_after_waiting,
    } => {
        let _ = window_size_seconds;
        let _ = retry_after_ms;
        let _ = remaining_after_waiting;
        // Stop (429 / backoff)
    }
    RateLimitDecision::Suppressed {
        suppression_factor,
        is_allowed,
    } => {
        let _ = suppression_factor;
        if is_allowed {
            // Proceed (maybe degraded)
        } else {
            // Stop (treat like 429)
        }
    }
}

What each decision means

Allowed

Allowed means this call is admitted.

  • For inc(...), the increment is recorded into the limiter state.
  • For is_allowed(...) (where available), no increment is recorded; it is a read-only admission preview.

Rejected

Rejected { window_size_seconds, retry_after_ms, remaining_after_waiting } means this call is denied.

  • The increment is not recorded.
  • The metadata is intended for backoff guidance and user-facing feedback.

Rejected fields (from the implementation):

  • window_size_seconds: the sliding window size used to compute this decision.
  • retry_after_ms: an estimate of milliseconds until capacity becomes available.
  • remaining_after_waiting: an estimate of window usage after waiting retry_after_ms.

How the hints are computed (sliding window with buckets):

  • retry_after_ms is derived from the oldest active bucket's remaining time in the window.
  • remaining_after_waiting is derived from subtracting the oldest bucket's count from the current total.

Because bucket coalescing merges nearby increments and concurrent requests can change bucket ages/totals, treat both values as hints, not guarantees.

Suppressed

Suppressed { suppression_factor, is_allowed } is returned by the suppressed strategy when it is actively applying probabilistic suppression.

  • suppression_factor is the current suppression rate (0.0 = no suppression, 1.0 = full suppression).
  • is_allowed is the admission decision for this specific call.

Implementation behavior (important for correct usage):

  • The suppressed strategy always tracks observed usage.
  • If a call is suppressed (is_allowed == false), that call is not recorded in the accepted series.
  • If a call is admitted (is_allowed == true), the accepted series is incremented.
  • If the request exceeds the hard limit, you will get Rejected { ... } (not Suppressed).

Rejection metadata

Rejected metadata is designed to answer two practical questions:

  • "How long should I wait before retrying?" -> retry_after_ms
  • "After waiting, how much usage will still be in the window?" -> remaining_after_waiting

HTTP mapping pattern

One common pattern is to turn decisions into 429 Too Many Requests responses:

use trypema::RateLimitDecision;

fn decision_to_status(decision: RateLimitDecision) -> (u16, Option<u64>) {
    match decision {
        RateLimitDecision::Allowed => (200, None),
        RateLimitDecision::Rejected {
            window_size_seconds: _,
            retry_after_ms,
            remaining_after_waiting: _,
        } => (429, Some(retry_after_ms)),
        RateLimitDecision::Suppressed {
            suppression_factor: _,
            is_allowed: true,
        } => (200, None),
        RateLimitDecision::Suppressed {
            suppression_factor: _,
            is_allowed: false,
        } => (429, None),
    }
}
With suppressed, you may choose to return 429 only when is_allowed == false, or degrade differently (queue, lower priority, partial responses).

End-to-end examples

Absolute strategy (Allowed / Rejected)

use trypema::{RateGroupSizeMs, RateLimit, RateLimitDecision, RateLimiter, RateLimiterOptions, SuppressionFactorCacheMs, WindowSizeSeconds};
use trypema::local::LocalRateLimiterOptions;

fn options() -> RateLimiterOptions {
    RateLimiterOptions {
        local: LocalRateLimiterOptions {
            window_size_seconds: WindowSizeSeconds::try_from(60).unwrap(),
            rate_group_size_ms: RateGroupSizeMs::try_from(10).unwrap(),
            hard_limit_factor: Default::default(),
            suppression_factor_cache_ms: SuppressionFactorCacheMs::default(),
        },
    }
}

let rl = RateLimiter::new(options());
let limiter = rl.local().absolute();

let rate = RateLimit::try_from(10.0).unwrap();

let decision = limiter.inc("user_123", &rate, 1);

match decision {
    RateLimitDecision::Allowed => {
        // Handle request
    }
    RateLimitDecision::Rejected {
        window_size_seconds,
        retry_after_ms,
        remaining_after_waiting,
    } => {
        let _ = window_size_seconds;
        let _ = remaining_after_waiting;
        // Return 429 and optionally include retry_after_ms
        let _ = retry_after_ms;
    }
    RateLimitDecision::Suppressed {
        suppression_factor: _,
        is_allowed: _,
    } => unreachable!("absolute strategy does not suppress"),
}

Suppressed strategy (Allowed / Suppressed / Rejected)

use trypema::{HardLimitFactor, RateGroupSizeMs, RateLimit, RateLimitDecision, RateLimiter, RateLimiterOptions, SuppressionFactorCacheMs, WindowSizeSeconds};
use trypema::local::LocalRateLimiterOptions;

fn options() -> RateLimiterOptions {
    RateLimiterOptions {
        local: LocalRateLimiterOptions {
            window_size_seconds: WindowSizeSeconds::try_from(60).unwrap(),
            rate_group_size_ms: RateGroupSizeMs::try_from(10).unwrap(),
            hard_limit_factor: HardLimitFactor::try_from(1.5).unwrap(),
            suppression_factor_cache_ms: SuppressionFactorCacheMs::default(),
        },
    }
}

let rl = RateLimiter::new(options());
let limiter = rl.local().suppressed();

let rate = RateLimit::try_from(10.0).unwrap();
let decision = limiter.inc("user_123", &rate, 1);

match decision {
    RateLimitDecision::Allowed => {
        // Below target capacity: proceed normally
    }
    RateLimitDecision::Suppressed {
        suppression_factor,
        is_allowed: true,
    } => {
        let _ = suppression_factor;
        // At/near target: this request admitted, consider degrading priority
    }
    RateLimitDecision::Suppressed {
        suppression_factor,
        is_allowed: false,
    } => {
        let _ = suppression_factor;
        // At/near target: this request suppressed, treat as denied (429)
    }
    RateLimitDecision::Rejected {
        window_size_seconds,
        retry_after_ms,
        remaining_after_waiting,
    } => {
        let _ = window_size_seconds;
        let _ = remaining_after_waiting;
        // Over hard limit: unconditionally denied
        let _ = retry_after_ms;
    }
}