trypema 1.1.0

High-performance rate limiting primitives in Rust, designed for concurrency safety, low overhead, and predictable latency.
Documentation

Trypema Rate Limiter

Crates.io Documentation License: MIT

Name and Biblical Inspiration

The name Trypema is derived from the Koine Greek word "τρυπήματος" (trypematos), meaning "hole" or "opening." It appears in the phrase "διὰ τρυπήματος ῥαφίδος" ("through the eye of a needle"), spoken by Jesus in three of the four Gospels:

  • Matthew 19:24"Again I tell you, it is easier for a camel to go through the eye of a needle than for someone who is rich to enter the kingdom of God."
  • Mark 10:25"It is easier for a camel to go through the eye of a needle than for someone who is rich to enter the kingdom of God."
  • Luke 18:25"Indeed, it is easier for a camel to go through the eye of a needle than for someone who is rich to enter the kingdom of God."

Just as the eye of a needle is a narrow passage that restricts what can pass through, a rate limiter is a narrow gate that controls the flow of requests into a system.

Overview

Trypema is a sliding-window rate limiting crate with:

  • Local in-memory limiting for single-process workloads
  • Redis best-effort distributed limiting with atomic Lua scripts
  • Hybrid best-effort distributed limiting with a local fast path and periodic Redis sync

Each provider offers:

  • Absolute deterministic allow/reject decisions
  • Suppressed probabilistic degradation near or above the target rate

Picking a Provider

Provider Best for Trade-off
Local single-process services, jobs, CLIs not shared across processes
Redis shared limits across processes or machines every check performs Redis I/O
Hybrid high-throughput distributed request paths state can lag behind Redis by sync_interval_ms

Redis and hybrid providers require Redis 7.2+ and exactly one runtime feature: redis-tokio or redis-smol.

Installation

Local-only:

[dependencies]
trypema = "1"

Redis with Tokio:

[dependencies]
trypema = { version = "1", features = ["redis-tokio"] }

Redis with Smol:

[dependencies]
trypema = { version = "1", features = ["redis-smol"] }

Quick Start

Common Types

RateLimit is the per-second limit value used by all providers. RedisKey is the validated key type required by the Redis and hybrid providers. RateLimitDecision is the result returned by inc() and is_allowed(), and RateLimiter is the top-level entry point used throughout the examples.

use trypema::RateLimit;
use trypema::redis::RedisKey;

let _rate_a = RateLimit::new(5.0).unwrap();
let _rate_b = RateLimit::try_from(5.0).unwrap();
let _rate_c = RateLimit::new_or_panic(5.0);

let _key_a = RedisKey::new("user_123".to_string()).unwrap();
let _key_b = RedisKey::try_from("user_123".to_string()).unwrap();
let _key_c = RedisKey::new_or_panic("user_123".to_string());

Create a RateLimiter

These examples show the local-only and Redis-enabled builder paths.

use trypema::{RateLimit, RateLimitDecision, RateLimiterBuilder};

let rl = RateLimiterBuilder::default()
    // Optional: override the sliding window size.
    .window_size_seconds(60)
    // Optional: override bucket coalescing.
    .rate_group_size_ms(10)
    // Optional: tune suppressed-mode headroom.
    .hard_limit_factor(1.5)
    // Optional: tune cleanup cadence.
    .cleanup_interval_ms(15_000)
    .build()
    .unwrap();

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

assert!(matches!(
    rl.local().absolute().inc("user_123", &rate, 1),
    RateLimitDecision::Allowed
));
use trypema::{RateLimit, RateLimitDecision, RateLimiter};

let rl = RateLimiter::builder().build().unwrap();
let rate = RateLimit::try_from(5.0).unwrap();

assert!(matches!(
    rl.local().absolute().inc("user_123", &rate, 1),
    RateLimitDecision::Allowed
));
use trypema::{RateLimit, RateLimitDecision, RateLimiter};
use trypema::redis::RedisKey;

async fn example() -> Result<(), trypema::TrypemaError> {
    let url = std::env::var("REDIS_URL")
        .unwrap_or_else(|_| "redis://127.0.0.1:6379/".to_string());
    let connection_manager = redis::Client::open(url)?
        .get_connection_manager()
        .await?;

    let rl = RateLimiter::builder(connection_manager)
        // Optional: override the sliding window size.
        .window_size_seconds(60)
        // Optional: override bucket coalescing.
        .rate_group_size_ms(10)
        // Optional: tune suppressed-mode headroom.
        .hard_limit_factor(1.5)
        // Optional: only available with `redis-tokio` or `redis-smol`.
        .redis_prefix(RedisKey::new_or_panic("docs".to_string()))
        // Optional: only available with `redis-tokio` or `redis-smol`.
        .sync_interval_ms(10)
        .build()?;

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

    assert!(matches!(
        rl.redis().absolute().inc(&key, &rate, 1).await?,
        RateLimitDecision::Allowed
    ));

    Ok(())
}
use std::sync::Arc;

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

let options = RateLimiterOptions {
    local: LocalRateLimiterOptions {
        window_size_seconds: WindowSizeSeconds::new_or_panic(60),
        rate_group_size_ms: RateGroupSizeMs::new_or_panic(10),
        hard_limit_factor: HardLimitFactor::new_or_panic(1.5),
        suppression_factor_cache_ms: SuppressionFactorCacheMs::new_or_panic(100),
    },
};

let rl = Arc::new(RateLimiter::new(options));
rl.run_cleanup_loop();

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

assert!(matches!(
    rl.local().absolute().inc("user_123", &rate, 1),
    RateLimitDecision::Allowed
));

build() starts the cleanup loop automatically. RateLimiter::new(...) does not, so call run_cleanup_loop() yourself when you want background cleanup of stale keys.

Local Read-Only Check

use trypema::{RateLimit, RateLimitDecision, RateLimiter};

let rl = RateLimiter::builder().build().unwrap();
let limiter = rl.local().absolute();
let rate = RateLimit::try_from(5.0).unwrap();

assert!(matches!(limiter.is_allowed("user_123"), RateLimitDecision::Allowed));
assert!(matches!(
    limiter.inc("user_123", &rate, 1),
    RateLimitDecision::Allowed
));

Read Current Suppression State

use trypema::RateLimiter;

let rl = RateLimiter::builder().build().unwrap();

let sf = rl.local().suppressed().get_suppression_factor("user_123");
assert_eq!(sf, 0.0);

Redis Absolute

use trypema::{RateLimit, RateLimitDecision, RateLimiter};
use trypema::redis::RedisKey;

async fn example() -> Result<(), trypema::TrypemaError> {
    let url = std::env::var("REDIS_URL")
        .unwrap_or_else(|_| "redis://127.0.0.1:6379/".to_string());
    let connection_manager = redis::Client::open(url)?
        .get_connection_manager()
        .await?;

    let rl = RateLimiter::builder(connection_manager).build()?;
    let key = RedisKey::try_from("user_123".to_string())?;
    let rate = RateLimit::try_from(5.0).unwrap();

    assert!(matches!(
        rl.redis().absolute().inc(&key, &rate, 1).await?,
        RateLimitDecision::Allowed
    ));

    Ok(())
}

Redis Suppressed State

use trypema::RateLimiter;
use trypema::redis::RedisKey;

async fn example() -> Result<(), trypema::TrypemaError> {
    let url = std::env::var("REDIS_URL")
        .unwrap_or_else(|_| "redis://127.0.0.1:6379/".to_string());
    let connection_manager = redis::Client::open(url)?
        .get_connection_manager()
        .await?;

    let rl = RateLimiter::builder(connection_manager).build()?;
    let key = RedisKey::try_from("user_123".to_string())?;

    assert_eq!(rl.redis().suppressed().get_suppression_factor(&key).await?, 0.0);

    Ok(())
}

Hybrid Absolute

use trypema::{RateLimit, RateLimitDecision, RateLimiter};
use trypema::redis::RedisKey;

async fn example() -> Result<(), trypema::TrypemaError> {
    let url = std::env::var("REDIS_URL")
        .unwrap_or_else(|_| "redis://127.0.0.1:6379/".to_string());
    let connection_manager = redis::Client::open(url)?
        .get_connection_manager()
        .await?;

    let rl = RateLimiter::builder(connection_manager).build()?;
    let key = RedisKey::try_from("user_123".to_string())?;
    let rate = RateLimit::try_from(10.0).unwrap();

    assert!(matches!(
        rl.hybrid().absolute().inc(&key, &rate, 1).await?,
        RateLimitDecision::Allowed
    ));

    Ok(())
}

Rate Limit Decisions

Every strategy returns RateLimitDecision:

  • Allowed means the request should proceed.
  • Rejected means the absolute strategy denied the request and includes best-effort backoff hints.
  • Suppressed means the suppressed strategy is active; check is_allowed for the admission result.

Notes

  • Rate limits are sticky per key: the first inc() stores the key's rate limit.
  • Bucket coalescing trades timing precision for lower overhead.
  • Redis and hybrid modes provide best-effort distributed limiting, not strict linearizability.

Testing Redis-Backed Docs

The canonical runnable examples live in the crate docs and API docs. Redis and hybrid doctests need a live Redis instance and REDIS_URL, for example:

REDIS_URL=redis://127.0.0.1:6379/ cargo test -p trypema --doc --features redis-tokio

Use redis-smol instead when validating the Smol-backed feature set.