salvo-rate-limiter
Salvo Rate Limiting
[dependencies]
salvo = { version = "0.89.3", features = ["rate-limiter"] }
Components
A RateLimiter combines four pieces:
| Component | Purpose | Built-ins |
|---|---|---|
RateIssuer |
Identify client | RemoteIpIssuer, RealIpIssuer |
RateGuard |
Limiting algorithm | FixedGuard (needs BasicQuota), SlidingGuard (needs CelledQuota) |
RateStore |
Persist state | MokaStore |
QuotaGetter |
Lookup quota for key | any Clone Quota, or custom impl |
Fixed Window by IP
use salvo::prelude::*;
use salvo::rate_limiter::{BasicQuota, FixedGuard, MokaStore, RateLimiter, RemoteIpIssuer};
#[handler]
async fn api() -> &'static str { "ok" }
#[tokio::main]
async fn main() {
let limiter = RateLimiter::new(
FixedGuard::default(),
MokaStore::default(),
RemoteIpIssuer,
BasicQuota::per_second(10),
);
let router = Router::new().hoop(limiter).push(Router::with_path("api").get(api));
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}
BasicQuota constructors
BasicQuota::per_second(10)
BasicQuota::per_minute(100)
BasicQuota::per_hour(1000)
BasicQuota::set_seconds(50, 30) // 50 per 30s
BasicQuota::set_minutes(500, 5)
BasicQuota::set_hours(5000, 2)
// Raw: BasicQuota::new(limit, time::Duration::seconds(n)) -- NOTE: time::Duration, not std::time
Sliding Window (smoother limiting)
SlidingGuard requires CelledQuota (limit split into N cells). Passing BasicQuota will not compile.
use salvo::rate_limiter::{CelledQuota, SlidingGuard, MokaStore, RateLimiter, RemoteIpIssuer};
let limiter = RateLimiter::new(
SlidingGuard::default(),
MokaStore::default(),
RemoteIpIssuer,
CelledQuota::per_minute(60, 6), // 60 req/min split into 6 x 10s cells
);
CelledQuota has the same per_/set_ constructors as BasicQuota but each takes an extra cells: usize parameter.
Behind a reverse proxy
RemoteIpIssuer uses the direct connection IP (the proxy). Use RealIpIssuer to read X-Forwarded-For / X-Real-IP:
use salvo::rate_limiter::RealIpIssuer;
let limiter = RateLimiter::new(
FixedGuard::default(), MokaStore::default(),
RealIpIssuer::new(),
BasicQuota::per_minute(100),
);
WARNING: only use RealIpIssuer behind a TRUSTED proxy that overwrites these headers, otherwise clients can forge them.
Custom issuer (user ID / API key)
RateIssuer::issue takes (&mut Request, &Depot):
use salvo::prelude::*;
use salvo::rate_limiter::RateIssuer;
struct UserIdIssuer;
impl RateIssuer for UserIdIssuer {
type Key = String;
async fn issue(&self, _req: &mut Request, depot: &Depot) -> Option<Self::Key> {
depot.get::<String>("user_id").ok().cloned()
}
}
struct ApiKeyIssuer;
impl RateIssuer for ApiKeyIssuer {
type Key = String;
async fn issue(&self, req: &mut Request, _depot: &Depot) -> Option<Self::Key> {
req.header::<String>("x-api-key")
}
}
Hybrid (user-if-authed, IP otherwise):
struct SmartIssuer;
impl RateIssuer for SmartIssuer {
type Key = String;
async fn issue(&self, req: &mut Request, depot: &Depot) -> Option<Self::Key> {
if let Ok(id) = depot.get::<String>("user_id") {
Some(format!("user:{id}"))
} else {
Some(format!("ip:{}", req.remote_addr().ip()?))
}
}
}
A closure Fn(&mut Request, &Depot) -> Option<K> also implements RateIssuer directly.
Dynamic per-user quotas
GOTCHA: QuotaGetter::get takes only &Q — no Depot. Look up quota by key alone (e.g. from a static map or DB).
use std::borrow::Borrow;
use std::hash::Hash;
use salvo::Error;
use salvo::rate_limiter::{BasicQuota, QuotaGetter};
struct TieredQuota;
impl QuotaGetter<String> for TieredQuota {
type Quota = BasicQuota;
type Error = Error;
async fn get<Q>(&self, key: &Q) -> Result<Self::Quota, Self::Error>
where
String: Borrow<Q>,
Q: Hash + Eq + Sync,
{
// Lookup tier by key (from DB, cache, etc.)
Ok(BasicQuota::per_minute(100))
}
}
Any Clone + Send + Sync quota type auto-implements QuotaGetter returning itself, which is why BasicQuota::per_second(10) works as the fourth argument directly.
Response headers
Enable built-in X-RateLimit-Limit / -Remaining / -Reset headers — no manual middleware needed:
let limiter = RateLimiter::new(FixedGuard::default(), MokaStore::default(),
RemoteIpIssuer, BasicQuota::per_minute(100))
.add_headers(true);
When the limit is exceeded, Salvo returns 429 Too Many Requests.
Per-route limits
let login_limiter = RateLimiter::new(FixedGuard::default(), MokaStore::default(),
RemoteIpIssuer, BasicQuota::per_minute(5));
let api_limiter = RateLimiter::new(FixedGuard::default(), MokaStore::default(),
RemoteIpIssuer, BasicQuota::per_minute(100));
let router = Router::new()
.push(Router::with_path("login").hoop(login_limiter).post(login))
.push(Router::with_path("api").hoop(api_limiter).get(api));
Skipper
Skip rate limiting for some requests:
let limiter = RateLimiter::new(/* ... */)
.with_skipper(|req: &mut Request, _: &Depot| {
req.uri().path().starts_with("/health")
});
Related Skills
- salvo-concurrency-limiter: limit concurrent requests
- salvo-auth: combine with authentication
- salvo-timeout: set timeouts alongside rate limits
More from salvo-rs/salvo-skills
salvo-csrf
Implement CSRF (Cross-Site Request Forgery) protection using cookie or session storage. Use for protecting forms and state-changing endpoints.
16salvo-auth
Implement authentication and authorization using JWT, Basic Auth, or custom schemes. Use for securing API endpoints and user management.
15salvo-cors
Configure Cross-Origin Resource Sharing (CORS) and security headers. Use for APIs accessed from browsers on different domains.
15salvo-caching
Implement caching strategies for improved performance. Use for reducing database load and speeding up responses.
15salvo-proxy
Implement reverse proxy to forward requests to backend services. Use for load balancing, API gateways, and microservices routing.
15salvo-timeout
Configure request timeouts to prevent slow requests from blocking resources. Use for protecting APIs from long-running operations.
15