Strategies

Suppressed Strategy

The probabilistic suppression strategy — gradually denies an increasing fraction of requests as traffic approaches and exceeds the limit.

The suppressed strategy is a probabilistic rate limiter that gracefully degrades under load. Instead of a hard cutoff (allowed/rejected), it computes a suppression factor and probabilistically denies a fraction of requests proportional to how far over the limit the key is. This produces smooth degradation rather than a cliff-edge rejection.

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

Inspired by Ably's distributed rate limiting at scale.

For a thorough explanation of the algorithm, suppression factor formula, and worked examples, see How Suppression Works.

When to use

  • Graceful degradation under load spikes (smooth ramp-up of denials instead of a cliff)
  • When you want to preserve some throughput for all clients rather than fully blocking at a threshold
  • Observability: the suppression factor tells you how close a key is to its limit
  • Load shedding with visibility into suppression rates for monitoring dashboards

If you want simple, binary allow/reject decisions, use the Absolute Strategy.

The three operating regimes

1. Below capacity

Condition: accepted_usage < window_size_seconds * rate_limit

All requests return RateLimitDecision::Allowed. No suppression is active.

2. At or near capacity (soft to hard limit)

Condition: Accepted usage is at or above the soft limit, but observed usage has not reached the hard limit.

A suppression factor between 0.0 and 1.0 is computed. Each request is probabilistically admitted with probability 1.0 - suppression_factor. Returns RateLimitDecision::Suppressed { is_allowed, suppression_factor }.

3. Over the hard limit

Condition: observed_usage >= window_size_seconds * rate_limit * hard_limit_factor

Full suppression. All requests return Suppressed { is_allowed: false, suppression_factor: 1.0 }.

The suppressed strategy never returns Rejected. Over the hard limit it returns Suppressed { is_allowed: false, suppression_factor: 1.0 }. Always check is_allowed to decide whether to proceed.

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(),
            // 1.5 = 50% burst headroom before full suppression
            hard_limit_factor: HardLimitFactor::try_from(1.5).unwrap(),
            suppression_factor_cache_ms: SuppressionFactorCacheMs::default(),
        },
    }));

    rl.run_cleanup_loop();

    // 10 req/s target. With hard_limit_factor = 1.5:
    //   Soft limit (window) = 60 * 10 = 600 requests
    //   Hard limit (window) = 60 * 10 * 1.5 = 900 requests
    let rate = RateLimit::try_from(10.0).unwrap();

    match rl.local().suppressed().inc("user_123", &rate, 1) {
        RateLimitDecision::Allowed => {
            // Below capacity. Proceed normally.
            println!("Allowed");
        }
        RateLimitDecision::Suppressed { is_allowed: true, suppression_factor } => {
            // Suppression is active, but this request passed through.
            // Proceed, but consider logging the factor for monitoring.
            println!("Allowed (suppression at {:.0}%)", suppression_factor * 100.0);
        }
        RateLimitDecision::Suppressed { is_allowed: false, suppression_factor } => {
            // This request was denied. Do NOT proceed.
            // When factor is 1.0, the key is over the hard limit.
            println!("Denied (suppression at {:.0}%)", suppression_factor * 100.0);
        }
        RateLimitDecision::Rejected { .. } => {
            // The suppressed strategy never returns Rejected.
            unreachable!();
        }
    }
}

Example: Redis provider

// #[tokio::main]
// async fn main() -> Result<(), trypema::TrypemaError> {
//     // ... (create rl with RedisRateLimiterOptions, same as quickstart-redis) ...
//
//     let key = RedisKey::try_from("user_123".to_string())?;
//     let rate = RateLimit::try_from(10.0)?;
//
//     match rl.redis().suppressed().inc(&key, &rate, 1).await? {
//         RateLimitDecision::Allowed => { /* proceed */ }
//         RateLimitDecision::Suppressed { is_allowed, suppression_factor } => {
//             if is_allowed {
//                 // proceed
//             } else {
//                 // deny
//             }
//         }
//         RateLimitDecision::Rejected { .. } => unreachable!(),
//     }
//
//     Ok(())
// }

The hard_limit_factor parameter

hard_limit_factor controls the gap between the "soft limit" (where suppression begins) and the "hard limit" (where full suppression kicks in).

soft_limit  = rate_limit                       (suppression begins)
hard_limit  = rate_limit * hard_limit_factor   (full suppression)

It is a validated newtype (HardLimitFactor): HardLimitFactor::try_from(value) fails if value < 1.0. Default is 1.0.

ValueMeaning
1.0 (default)No headroom. Suppression starts and reaches 1.0 at the same point (hard cutoff, similar to absolute).
1.550% headroom. Suppression gradually ramps from 0 to 1 over the range rate_limit to rate_limit * 1.5.
2.0100% headroom. Even more gradual ramp.

Recommended starting point: hard_limit_factor = 1.5.

In window terms:

soft_window_limit = window_size_seconds * rate_limit
hard_window_limit = window_size_seconds * rate_limit * hard_limit_factor

The get_suppression_factor() method

All three providers expose a method to query the current suppression factor for a key without recording any increment:

// Local (sync)
// let factor = rl.local().suppressed().get_suppression_factor("user_123");

// Redis (async)
// let factor = rl.redis().suppressed().get_suppression_factor(&key).await?;

// Hybrid (async)
// let factor = rl.hybrid().suppressed().get_suppression_factor(&key).await?;

Returns a value in [0.0, 1.0]:

  • 0.0 -- no suppression (below capacity or key not found)
  • 0.0 < sf < 1.0 -- partial suppression
  • 1.0 -- full suppression (over hard limit)

Use this for observability, dashboards, and debugging.

Configuration

The suppressed strategy uses all configuration options:

OptionEffect on suppressed strategy
window_size_secondsLength of the sliding window.
rate_group_size_msBucket coalescing interval.
hard_limit_factorControls the headroom range between soft and hard limits.
suppression_factor_cache_msHow long the computed factor is cached before recomputing.

Next steps