Decisions
All admission checks return a RateLimitDecision:
Allowed: admitted and recordedRejected { window_size_seconds, retry_after_ms, remaining_after_waiting }: denied and not recorded, with backoff hintsSuppressed { suppression_factor, is_allowed }: suppressed strategy only; you must gate onis_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,
},
}
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 waitingretry_after_ms.
How the hints are computed (sliding window with buckets):
retry_after_msis derived from the oldest active bucket's remaining time in the window.remaining_after_waitingis 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_factoris the current suppression rate (0.0 = no suppression, 1.0 = full suppression).is_allowedis 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 { ... }(notSuppressed).
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),
}
}
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;
}
}

