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/TrypemaREADME
Trypema Rate Limiter
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:
Allowedmeans the request should proceed.Rejectedmeans the absolute strategy denied the request and includes best-effort backoff hints.Suppressedmeans the suppressed strategy is active; checkis_allowedfor 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
openhue
351.8kControl Philips Hue lights and scenes via the OpenHue CLI.
sag
351.8kElevenLabs text-to-speech with mac-style say UX.
weather
351.8kGet current weather and forecasts via wttr.in or Open-Meteo
casdoor
13.3kAn open-source AI-first Identity and Access Management (IAM) /AI MCP & agent gateway and auth server with web UI supporting OpenClaw, MCP, OAuth, OIDC, SAML, CAS, LDAP, SCIM, WebAuthn, TOTP, MFA, Face ID, Google Workspace, Azure AD
