SkillAgentSearch skills...

Trypema

Trypema is a Rust rate limiting library that supports both in-process and Redis-backed distributed enforcement. It provides two complementary strategies — strict enforcement and probabilistic suppression — across three provider backends.

Install / Use

/learn @dev-davexoyinbo/Trypema
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

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

Related Skills

View on GitHub
GitHub Stars10
CategoryCustomer
Updated8h ago
Forks0

Languages

Rust

Security Score

90/100

Audited on Apr 8, 2026

No findings