Strategies

Absolute Strategy

The deterministic sliding-window strategy — requests under capacity are allowed, requests over it are immediately rejected.

The absolute strategy is a deterministic sliding-window rate limiter. It makes binary decisions: requests under the window capacity are allowed, requests over it are immediately rejected. Simple, predictable, and easy to reason about.

Access: rl.local().absolute(), rl.redis().absolute(), or rl.hybrid().absolute()

How it works

  1. Compute the window capacity: window_size_seconds * rate_limit
  2. Sum all bucket counts within the current sliding window
  3. If total < capacity --> allow the request and record the increment
  4. If total >= capacity --> reject the request (increment is not recorded)

When to use

  • Simple per-key rate caps (e.g., API rate limiting with a fixed quota)
  • Scenarios where predictable, binary enforcement matters more than graceful degradation
  • When you want rejected requests to include backoff hints (retry_after_ms)

If you want smooth degradation instead of a hard cutoff, use the Suppressed Strategy.

Example: Local provider

use std::sync::Arc;

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

fn main() {
    let rl = Arc::new(RateLimiter::new(RateLimiterOptions {
        local: LocalRateLimiterOptions {
            window_size_seconds: WindowSizeSeconds::try_from(60).unwrap(),
            rate_group_size_ms: RateGroupSizeMs::try_from(10).unwrap(),
            hard_limit_factor: HardLimitFactor::default(), // not used by absolute
            suppression_factor_cache_ms: SuppressionFactorCacheMs::default(),
        },
    }));

    rl.run_cleanup_loop();

    let rate = RateLimit::try_from(5.0).unwrap();
    // Window capacity = 60 * 5.0 = 300 requests

    // Record a request
    match rl.local().absolute().inc("user_123", &rate, 1) {
        RateLimitDecision::Allowed => {
            println!("Request allowed");
        }
        RateLimitDecision::Rejected { retry_after_ms, remaining_after_waiting, .. } => {
            println!(
                "Rejected. Retry in ~{}ms ({} will remain).",
                retry_after_ms, remaining_after_waiting
            );
        }
        RateLimitDecision::Suppressed { .. } => {
            unreachable!("absolute strategy never returns Suppressed");
        }
    }
}

Example: Redis provider

use std::sync::Arc;

use trypema::{
    HardLimitFactor, RateGroupSizeMs, RateLimit, RateLimitDecision,
    RateLimiter, RateLimiterOptions, SuppressionFactorCacheMs, WindowSizeSeconds,
};
use trypema::hybrid::SyncIntervalMs;
use trypema::local::LocalRateLimiterOptions;
use trypema::redis::{RedisKey, RedisRateLimiterOptions};

// #[tokio::main]
// async fn main() -> Result<(), trypema::TrypemaError> {
//     let client = redis::Client::open("redis://127.0.0.1:6379/").unwrap();
//     let connection_manager = client.get_connection_manager().await.unwrap();
//
//     let rl = Arc::new(RateLimiter::new(RateLimiterOptions {
//         local: LocalRateLimiterOptions { /* ... */ },
//         redis: RedisRateLimiterOptions {
//             connection_manager,
//             prefix: None,
//             /* ... same fields as local, plus sync_interval_ms ... */
//         },
//     }));
//
//     let key = RedisKey::try_from("user_123".to_string())?;
//     let rate = RateLimit::try_from(5.0)?;
//
//     match rl.redis().absolute().inc(&key, &rate, 1).await? {
//         RateLimitDecision::Allowed => { /* proceed */ }
//         RateLimitDecision::Rejected { retry_after_ms, .. } => {
//             println!("Rejected. Retry in ~{}ms", retry_after_ms);
//         }
//         RateLimitDecision::Suppressed { .. } => unreachable!(),
//     }
//
//     Ok(())
// }

The is_allowed() method

The local absolute strategy also provides an is_allowed(key) method that checks whether the key is currently under its limit without recording an increment. This is useful for:

  • Preview: Check before doing expensive work, then call inc() only if you proceed.
  • Metrics: Sample rate limit status without affecting state.
// Check without incrementing
// match rl.local().absolute().is_allowed("user_123") {
//     RateLimitDecision::Allowed => {
//         // Do expensive work, then record
//         // expensive_operation();
//         // rl.local().absolute().inc("user_123", &rate, 1);
//     }
//     RateLimitDecision::Rejected { .. } => {
//         // Skip the expensive work
//     }
//     _ => unreachable!(),
// }
is_allowed() performs lazy eviction of expired buckets as a side-effect, but does not modify counters. It is available on the local absolute strategy. The Redis absolute strategy provides it as an async method.

Rejection metadata

When a request is rejected, the decision includes best-effort hints:

FieldDescription
window_size_secondsThe sliding window size used for this decision.
retry_after_msEstimated milliseconds until capacity opens up (based on oldest bucket's TTL).
remaining_after_waitingEstimated requests still in the window after the oldest bucket expires (total - oldest_bucket_count).

These are approximate. See Decisions for details.

Behaviour under concurrency

The admission check and the increment are not a single atomic operation. Under high concurrency, multiple threads can observe "allowed" simultaneously and all proceed, causing temporary overshoot. This is by design -- the alternative (per-key locking) would significantly reduce throughput.

If you need strict serialisation, use external synchronization (e.g., per-key mutexes).

Configuration

The absolute strategy uses window_size_seconds and rate_group_size_ms from the provider options. It ignores hard_limit_factor and suppression_factor_cache_ms (those are for the suppressed strategy).

See Configuration & Tuning for details on every option.

Next steps