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.
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
| 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.
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"] }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());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.
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
));use trypema::RateLimiter;
let rl = RateLimiter::builder().build().unwrap();
let sf = rl.local().suppressed().get_suppression_factor("user_123");
assert_eq!(sf, 0.0);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(())
}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(())
}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(())
}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.
- 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.
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-tokioUse redis-smol instead when validating the Smol-backed feature set.