Skip to content

RateLimiter

Rate limiting lives in serenade-rate-limiter (#186, #192, #193).

Symfony RateLimiter analogue for login / API / form abuse. Serenade owns policies and storage; apps choose which keys and routes to limit.

Type Role
Policy Token bucket or fixed window (limit + interval)
RateLimiterFactory / RateLimiter Create per-subject handles; consume / reset
RateLimiterStorage / WindowState Persist window snapshots
RateLimit Accept / remaining tokens / retry_after
RateLimiterError Invalid key / tokens / storage failures
Store Role
InMemoryRateLimiterStorage Process-local (default DI)
RedisRateLimiterStorage (feature redis) Multi-node windows via Redis keys (unix-millis timestamps; Instant bridged at the storage boundary)
use serenade_rate_limiter::{RedisRateLimiterStorage, RedisRateLimiterStorageConfig};
let storage = RedisRateLimiterStorage::connect(
RedisRateLimiterStorageConfig::new("redis://127.0.0.1:6379/0")
.with_prefix("serenade:rate-limiter:")
.with_warm_pool(true),
)?;

Requires a live Redis and crate feature redis (redis-rs + r2d2). Consume uses WATCH/MULTI so concurrent nodes share the same budget.

Helper Role
require_accepted Turn a rejected RateLimit into RateLimitExceeded
consume_or_exceed consume then require acceptance
too_many_requests Build HTTP 429 with Retry-After and X-RateLimit-*

FrameworkExtension adds [RegisterDefaultRateLimiterPass], which registers service id rate_limiter.storage (DEFAULT_RATE_LIMITER_STORAGE_SERVICE) tagged rate_limiter.storage with an InMemoryRateLimiterStorage when missing. Resolve RateLimiterStorageService and call factory / fixed_window_factory / token_bucket_factory.

use std::sync::Arc;
use std::time::Duration;
use serenade_rate_limiter::{
InMemoryRateLimiterStorage, Policy, RateLimiterFactory, RateLimiterStorage,
consume_or_exceed, too_many_requests,
};
let storage = Arc::new(InMemoryRateLimiterStorage::new());
let factory = RateLimiterFactory::new(
"login",
Policy::fixed_window(5, Duration::from_secs(15 * 60))?,
storage,
)?;
let limiter = factory.create("alice")?;
match consume_or_exceed(&limiter, 1) {
Ok(_) => { /* proceed */ }
Err(serenade_rate_limiter::ConsumeOrExceedError::Exceeded(exceeded)) => {
let _response = too_many_requests(&exceeded.rate_limit);
}
Err(other) => return Err(other.into()),
}