Trypema
High-performance sliding-window rate limiting primitives for Rust, designed for concurrency safety, low overhead, and predictable latency.
Trypema offers two strategies:
- Absolute: rejects requests after the configured window capacity is reached.
- Suppressed: progressively suppresses traffic between a soft target and a hard cutoff.
And three independently constructed providers:
- Local: in-process, synchronous, always available.
- Redis: distributed, async, one atomic Redis script per operation.
- Hybrid: local fast path with mandatory periodic Redis synchronization.
Install
[]
= "2"
For Redis or hybrid providers, enable exactly one runtime feature:
= { = "2", = ["redis-tokio"] }
# or: features = ["redis-smol"]
Redis-backed providers require Redis 7.2 or newer.
Local example
use Duration;
use ;
let provider = builder
.window_size
.bucket_size
.cleanup_interval
.build
.unwrap;
let rate = per_second_or_panic;
let decision = provider.absolute.inc;
assert!;
All concrete builders implement the shared RateLimiterBuilder trait. build() returns an
Arc and starts stale-state cleanup by default. Use .disable_cleanup() to opt out, or control
conditional builder configuration with .enable_cleanup(). After construction, use the provider's
idempotent start_cleanup_loop() and stop_cleanup_loop() methods.
Redis and hybrid construction
use ;
use ;
let connection = open?
.get_connection_manager
.await?;
let provider = builder
.prefix
.window_size
.bucket_size
.build?;
Hybrid adds a provider-specific synchronization interval:
use ;
let provider = builder
.sync_interval
.build?;
Redis and hybrid operations accept &RedisKey. Local operations accept &str. Invalid Redis
keys are rejected; keys are never silently sanitized.
Semantic time values
The public API uses validated, unit-aware values:
| Type | Constructors |
|---|---|
RateLimit |
per_second, per_minute, per_hour, per_day, per_week, per_month |
WindowSize |
seconds, minutes, hours, days, weeks, months |
BucketSize |
milliseconds, seconds, minutes, hours, days, weeks, months |
SuppressionFactorCachePeriod |
milliseconds, seconds, minutes, hours, days |
hybrid::SyncInterval |
milliseconds, seconds, minutes, hours |
Provider builders require bucket_size to be less than or equal to window_size; equality is
valid. The relationship is checked by build(), regardless of setter order.
One month is defined as 30 days. Every constructor has an _or_panic counterpart. RateLimit
provides matching as_per_second(), as_per_minute(), as_per_hour(), as_per_day(),
as_per_week(), and as_per_month() getters. WindowSize provides as_seconds(),
as_milliseconds(), as_minutes(), as_hours(), as_days(), as_weeks(), and as_months();
the minute-and-larger getters return f64. SyncInterval provides as_milliseconds(),
as_seconds(), as_minutes(), and as_hours(); the second-and-larger getters return f64.
These getters expose values without leaking or permitting mutation of their representation.
Conditional updates
set_if and set_if_preserve_history return ConditionalSetOutcome:
matcheddistinguishes a comparator miss from a successful no-op.previous_totalis the live total used by the comparison.current_totalis the total after the operation.
Use RateLimitComparator::Always for an unconditional update through the conditional path.
Matched zero targets delete state. Matched updates also replace the sticky window capacity.
set_if_preserve_history additionally retains one side of live history:
PreserveNewestconsumes oldest buckets first; increases extend newest history.PreserveOldestconsumes newest buckets first; increases extend oldest history.
Managing existing keys
Every limiter exposes set_rate_limit, delete, and clear. set_rate_limit changes only an
existing key's sticky capacity and returns its previous effective RateLimit; it preserves live
history, totals, timestamps, and declined usage. Missing keys return None, and an equivalent
effective rate is a no-op. Changed suppressed rates invalidate their cached suppression factor.
delete removes one key and returns its live pre-delete usage in Some: the live total for
absolute limiters and accepted usage (total - total_declined) for suppressed limiters. Existing
zero-usage or cache-only state returns Some(0); missing state returns None. clear removes all
keys for only the called strategy and configured prefix. Local methods are synchronous; Redis and
hybrid methods are asynchronous and return Result. Hybrid revisions prevent stale cached totals
and limits from overriding these mutations, but concurrent or remote pending increments can still
recreate a key afterward.
Decisions and behavior
RateLimitDecision::Rejected exposes window_size: WindowSize, retry_after: Duration, and
remaining_after_waiting. Metadata is best-effort under bucket coalescing and concurrency.
Suppressed decisions expose is_allowed and suppression_factor.
Absolute get returns the live total as u64. Suppressed get returns
SuppressedRateLimitSnapshot with observed usage, declined usage, and suppression factor.
Unknown keys return zero-valued results without creating state. Hybrid reads include this
instance's pending local counts; get_estimate may answer from local state without Redis I/O.
RateLimitDecision is exhaustive, while the fields of its Rejected and Suppressed variants
are non-exhaustive; match those variants with { .. }. Other public result structs and the error
model are non-exhaustive. Absolute admission is best-effort under concurrency and may temporarily
overshoot. Reads report live state and may lazily evict expired buckets.
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.