Strategies

Absolute

Strict sliding-window enforcement.

Absolute is a deterministic sliding-window limiter.

It is the best default when you want predictable "allow/reject" behavior.

Behavior:

Below capacity it returns Allowed. Over capacity it returns Rejected with backoff hints.

Local absolute stores the rate limit per key when the key is first seen. For stable behavior, treat the rate limit as fixed per key.

Usage (Local)

Local absolute is synchronous:

use std::sync::Arc;

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

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(),
        suppression_factor_cache_ms: SuppressionFactorCacheMs::default(),
    },
}));

rl.run_cleanup_loop();

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

let decision = rl.local().absolute().inc("user_123", &rate, 1);

match decision {
    RateLimitDecision::Allowed => {
        // proceed
    }
    RateLimitDecision::Rejected {
        window_size_seconds,
        retry_after_ms,
        remaining_after_waiting,
    } => {
        let _ = window_size_seconds;
        let _ = remaining_after_waiting;
        // backoff / return 429
        let _ = retry_after_ms;
    }
    RateLimitDecision::Suppressed {
        suppression_factor: _,
        is_allowed: _,
    } => unreachable!("absolute strategy does not suppress"),
}

Read-only check (preview) is available on local absolute:

use trypema::{RateLimit, RateLimitDecision};

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

match rl.local().absolute().is_allowed("user_123") {
    RateLimitDecision::Allowed => {
        // do expensive work, then record the request
        let _ = rl.local().absolute().inc("user_123", &rate, 1);
    }
    RateLimitDecision::Rejected {
        window_size_seconds: _,
        retry_after_ms,
        remaining_after_waiting: _,
    } => {
        let _ = retry_after_ms;
        // deny / delay work
    }
    RateLimitDecision::Suppressed {
        suppression_factor: _,
        is_allowed: _,
    } => unreachable!("absolute strategy does not suppress"),
}

Usage (Redis)

Redis absolute is asynchronous and returns Result<RateLimitDecision, TrypemaError>.

use std::sync::Arc;

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

// Create Redis connection manager
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 {
        window_size_seconds: WindowSizeSeconds::try_from(60).unwrap(),
        rate_group_size_ms: RateGroupSizeMs::try_from(10).unwrap(),
        hard_limit_factor: HardLimitFactor::default(),
        suppression_factor_cache_ms: SuppressionFactorCacheMs::default(),
    },
    redis: RedisRateLimiterOptions {
        connection_manager,
        prefix: None,
        window_size_seconds: WindowSizeSeconds::try_from(60).unwrap(),
        rate_group_size_ms: RateGroupSizeMs::try_from(10).unwrap(),
        hard_limit_factor: HardLimitFactor::default(),
        suppression_factor_cache_ms: SuppressionFactorCacheMs::default(),
    },
}));

rl.run_cleanup_loop();

let key = RedisKey::try_from("user_123".to_string()).unwrap();
let rate = RateLimit::try_from(10.0).unwrap();

let decision = rl.redis().absolute().inc(&key, &rate, 1).await.unwrap();

match decision {
    RateLimitDecision::Allowed => {
        // proceed
    }
    RateLimitDecision::Rejected {
        window_size_seconds,
        retry_after_ms,
        remaining_after_waiting,
    } => {
        let _ = window_size_seconds;
        let _ = remaining_after_waiting;
        // backoff / return 429
        let _ = retry_after_ms;
    }
    RateLimitDecision::Suppressed {
        suppression_factor: _,
        is_allowed: _,
    } => unreachable!("absolute strategy does not suppress"),
}

Read-only check (preview) is also available on Redis absolute:

use trypema::RateLimitDecision;
use trypema::redis::RedisKey;

let key = RedisKey::try_from("user_123".to_string()).unwrap();

match rl.redis().absolute().is_allowed(&key).await.unwrap() {
    RateLimitDecision::Allowed => {
        // allowed at this instant
    }
    RateLimitDecision::Rejected {
        window_size_seconds: _,
        retry_after_ms,
        remaining_after_waiting: _,
    } => {
        let _ = retry_after_ms;
        // backoff
    }
    RateLimitDecision::Suppressed {
        suppression_factor: _,
        is_allowed: _,
    } => unreachable!("absolute strategy does not suppress"),
}

Algorithm (high level)

For each key, absolute computes a window capacity (window_size_seconds * rate_limit), checks current usage in the last window, rejects if adding count would exceed capacity, otherwise records the increment (coalescing into a recent bucket when applicable).

Concurrency semantics

  • Local absolute can temporarily overshoot under contention: inc(...) performs an admission check and then records the increment, so concurrent callers may temporarily overshoot.
  • Redis absolute inc(...) runs the admission check and increment inside a single Lua script, so concurrent callers for the same key do not overshoot due to races.

If you do a separate is_allowed(...) check and then later call inc(...), that check-then-act pattern is not atomic for either provider.

When to use

Use absolute when you need a hard cap per key, strict rejection semantics, and easy mapping to 429 responses with a retry hint.