pub struct AbsoluteLocalRateLimiter { /* private fields */ }Expand description
Strict sliding-window rate limiter for in-process use.
Provides deterministic rate limiting with per-key state maintained in memory. Uses a sliding time window to track request counts and enforce limits.
§Algorithm
- Window capacity:
window_size_seconds × rate_limit - Admission check: Sum all bucket counts within the window
- Decision: Allow if
total < capacity, reject otherwise - Increment: If allowed, add count to current (or coalesced) bucket
§Thread Safety
- Uses
DashMapfor concurrent key access - Uses atomics for per-bucket counters
- Safe for multi-threaded use without external synchronization
§Semantics & Limitations
Sticky rate limits:
- The first call for a key stores the rate limit
- Subsequent calls for the same key do not update it
- Rationale: Avoids races where concurrent calls specify different limits
Best-effort concurrency:
- Admission check and increment are not atomic across calls
- Multiple threads can observe “allowed” simultaneously
- All may proceed, causing temporary overshoot
- This is expected behavior, not a bug
Eviction granularity:
- Uses
Instant::elapsed().as_millis()(whole-millisecond truncation) - Buckets expire close to
window_size_seconds(lazy eviction may delay removal until next call)
Memory growth:
- Keys are not automatically removed
- Unbounded key cardinality will grow memory
- Use
run_cleanup_loop()to periodically remove stale keys
Lazy eviction:
- Expired buckets are only removed when
is_allowed()orinc()is called - Stale buckets remain in memory until accessed or cleanup runs
§Performance
- Admission check: O(buckets_in_window) — typically < 10 buckets
- Increment: O(1) amortised (coalesced into existing bucket or appended via
push_back) - Memory: ~50–200 bytes per key (depends on bucket count)
§Examples
use trypema::{RateLimit, RateLimitDecision};
let limiter = rl.local().absolute();
let rate = RateLimit::try_from(10.0).unwrap();
assert!(matches!(limiter.inc("user_123", &rate, 1), RateLimitDecision::Allowed));
assert!(matches!(limiter.is_allowed("user_123"), RateLimitDecision::Allowed));Implementations§
Source§impl AbsoluteLocalRateLimiter
impl AbsoluteLocalRateLimiter
Sourcepub fn inc(
&self,
key: &str,
rate_limit: &RateLimit,
count: u64,
) -> RateLimitDecision
pub fn inc( &self, key: &str, rate_limit: &RateLimit, count: u64, ) -> RateLimitDecision
Check admission and, if allowed, record the increment for key.
This is the primary method for rate limiting. It performs an admission check and, if allowed, records the increment in the key’s state.
§Arguments
key: Unique identifier for the rate-limited resource (e.g.,"user_123","api_endpoint")rate_limit: Per-second rate limit. Sticky: stored on first call, ignored on subsequent callscount: Amount to increment (typically1for single requests, or batch size)
§Returns
RateLimitDecision::Allowed: Request admitted, increment recordedRateLimitDecision::Rejected: Over limit, increment not recorded
§Behavior
- Check current window usage via
is_allowed(key) - If over limit, return
Rejected(no state change) - If allowed:
- Check if recent bucket exists within
rate_group_size_ms - If yes: add count to existing bucket (coalescing)
- If no: create new bucket with count
- Return
Allowed
- Check if recent bucket exists within
§Concurrency
Not atomic across calls. Under concurrent load:
- Multiple threads may observe
Allowedsimultaneously - All may proceed and increment, causing temporary overshoot
- This is expected and by design for performance
For strict enforcement, use external synchronization (e.g., per-key locks).
§Bucket Coalescing
Increments within rate_group_size_ms of the most recent bucket are merged
into that bucket. This reduces memory usage and improves performance.
§Examples
use trypema::{RateLimit, RateLimitDecision};
let limiter = rl.local().absolute();
let rate = RateLimit::try_from(10.0).unwrap();
// Single request
assert!(matches!(limiter.inc("user_123", &rate, 1), RateLimitDecision::Allowed));
// Batch of 10
assert!(matches!(limiter.inc("user_456", &rate, 10), RateLimitDecision::Allowed));Sourcepub fn is_allowed(&self, key: &str) -> RateLimitDecision
pub fn is_allowed(&self, key: &str) -> RateLimitDecision
Check if key is currently under its rate limit (read-only).
Performs an admission check without recording an increment. Useful for previewing whether a request would be allowed before doing expensive work.
§Arguments
key: Unique identifier for the rate-limited resource
§Returns
RateLimitDecision::Allowed: Key is under limitRateLimitDecision::Rejected: Key is over limit, includes backoff hints
§Behavior
- If key doesn’t exist, return
Allowed(no state yet) - Perform lazy eviction of expired buckets
- Sum remaining bucket counts
- Compare against
window_capacity = window_size_seconds × rate_limit - Return decision with metadata if rejected
§Side Effects
- Lazy eviction: Removes expired buckets from key’s state
- No increment: Does not modify counters (read-only check)
§Use Cases
- Preview: Check before expensive operations
- Metrics: Sample rate limit status without affecting state
- Testing: Verify rate limit behavior
§Examples
use trypema::{RateLimit, RateLimitDecision};
let limiter = rl.local().absolute();
let rate = RateLimit::try_from(10.0).unwrap();
// Unknown key → always allowed
assert!(matches!(limiter.is_allowed("new_key"), RateLimitDecision::Allowed));
// Check before recording
if matches!(limiter.is_allowed("user_123"), RateLimitDecision::Allowed) {
limiter.inc("user_123", &rate, 1);
}