Rate limits
Rate limits per key kept in memory (GCRA): allowed or retry after so many milliseconds, for sign-ins and APIs.
Version 1.0.0 · 3 functions · 0 crates
Functions
What use rate_limit.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use rate_limit, rate_limit_burst from rate_limit (Quillo 0.6).
rate_limit(key: str, limit: int, period_seconds: int) → anyor an errorCounts one request for key against limit requests per period_seconds: { allowed, retry_after_ms, remaining, limit }. The whole limit may come at once, then they refill evenly (10 per 60 s: one more every 6 s). A refused request does not count. Limits live in this process: each server of several keeps its own, and a restart forgets them.
rate_limit_burst(key: str, rate: int, period_seconds: int, burst: int, cost: int) → anyor an errorAs rate_limit, with the refill (rate per period_seconds) and the burst (how many at once) set apart, and a cost for requests that weigh more than one (an export costing 10). A cost of 0 only looks.
rate_limit_reset(key: str) → boolForgets key, so its next request starts full: after a successful sign-in, say. Whether it was there.
Get it
$ quillo install rate_limit # rate_limit.qwer into this folder$ quillo install rate_limit --example # and its example, rate_limit.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/rate_limit.qwer$ curl -O https://quillolang.org/plugins/files/rate_limit.qweThen, in your program, all of it or only what you call:
use rate_limit.qweruse rate_limit, rate_limit_burst from rate_limit // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
Example
// A sign-in that allows 5 tries a minute per IP, and an API limited per key.//// quillo run rate_limit.qwe then open http://localhost:8080/burstuse "rate_limit.qwer"fn limited(req) { let r = rate_limit(`api:${req.headers["x-api-key"] ?? req.ip}`, 100, 60)? if r.allowed { return req } respond(429, { error: "slow down", retry_after_ms: r.retry_after_ms }, { "retry-after": str(ceil(r.retry_after_ms / 1000)) })}routes /api | limited { route GET /orders = [{ id: 1, total: 120.5 }]}route POST /login { form: { email: str, password: str } } { let r = rate_limit(`login:${request.ip}`, 5, 60)? if !r.allowed { reply 429 `Too many tries: wait ${ceil(r.retry_after_ms / 1000)} s.` } { signed_in: false }}// Seven tries in a row from one visitor: five pass, two are told to wait.route GET /burst { rate_limit_reset("demo") let tries = range(7) | map((i) => rate_limit("demo", 5, 60)?) { allowed: len(tries | filter((t) => t.allowed)), refused: len(tries | filter((t) => !t.allowed)), wait_ms: last(tries).retry_after_ms }}$ quillo run rate_limit.qwe$ curl 'localhost:8080/burst'$ curl 'localhost:8080/api/orders'Source
//! Rate limits kept in the server's memory, per key (an IP, a user, an API key): GCRA, as Redis-cell and Stripe use, with "allowed, or retry after so many ms".//! version: 1.0.0//!//! [dependencies]use serde_json::{json, Value};use std::collections::HashMap;use std::sync::{Mutex, OnceLock};use std::time::Instant;/// At most this many keys are kept; past it, keys whose limit has fully refilled are dropped first.const MAX_KEYS: usize = 200_000;struct State { start: Instant, /// For each key, its theoretical arrival time in ms since `start`. tat: HashMap<String, f64>,}fn state() -> &'static Mutex<State> { static S: OnceLock<Mutex<State>> = OnceLock::new(); S.get_or_init(|| Mutex::new(State { start: Instant::now(), tat: HashMap::new() }))}fn take(key: &str, rate: i64, period_ms: f64, burst: i64, cost: i64) -> Result<Value, String> { if rate < 1 || period_ms <= 0.0 { return Err("the rate is at least 1 per period, and the period above 0".into()); } let burst = burst.clamp(1, 1_000_000) as f64; let cost = cost.clamp(0, 1_000_000) as f64; let interval = period_ms / rate as f64; let mut s = state().lock().unwrap_or_else(|e| e.into_inner()); let now = s.start.elapsed().as_secs_f64() * 1000.0; if s.tat.len() >= MAX_KEYS && !s.tat.contains_key(key) { s.tat.retain(|_, t| *t > now); if s.tat.len() >= MAX_KEYS { return Err("too many keys are being limited at once".into()); } } let tat = s.tat.get(key).copied().unwrap_or(now).max(now); let new_tat = tat + interval * cost; let allow_at = new_tat - interval * burst; if now < allow_at { let left = ((now - (tat - interval * burst)) / interval).floor().max(0.0); return Ok(json!({ "allowed": false, "retry_after_ms": (allow_at - now).ceil() as i64, "remaining": left as i64, "limit": burst as i64 })); } s.tat.insert(key.to_string(), new_tat); let left = ((now - allow_at) / interval).floor().max(0.0); Ok(json!({ "allowed": true, "retry_after_ms": 0, "remaining": left as i64, "limit": burst as i64 }))}/// Counts one request for `key` against `limit` requests per `period_seconds`: `{ allowed, retry_after_ms, remaining, limit }`./// The whole `limit` may come at once, then they refill evenly (10 per 60 s: one more every 6 s). A refused request does not count./// Limits live in this process: each server of several keeps its own, and a restart forgets them.pub fn rate_limit(key: &str, limit: i64, period_seconds: i64) -> Result<Value, String> { take(key, limit, period_seconds as f64 * 1000.0, limit, 1)}/// As rate_limit, with the refill (`rate` per `period_seconds`) and the burst (how many at once) set apart, and a `cost` for/// requests that weigh more than one (an export costing 10). A cost of 0 only looks.pub fn rate_limit_burst(key: &str, rate: i64, period_seconds: i64, burst: i64, cost: i64) -> Result<Value, String> { take(key, rate, period_seconds as f64 * 1000.0, burst, cost)}/// Forgets `key`, so its next request starts full: after a successful sign-in, say. Whether it was there.pub fn rate_limit_reset(key: &str) -> bool { state().lock().unwrap_or_else(|e| e.into_inner()).tat.remove(key).is_some()}Related
biscuit.qwer · 1.0.0Biscuit authorization tokens: issued with Datalog facts and checks signed by your key, narrowed by their holder without asking the issuer, and authorized offline against each request.7 views · 8 downloadsJWKS jwks.qwer · 1.0.0Verify sign-in tokens from Auth0, Okta, Entra ID, Cognito or Keycloak against the provider's JWKS keys, picked by kid.8 views · 6 downloadsJWT jwt.qwer · 2.0.0JSON Web Tokens: sign and verify HS256 tokens for an API, check a token's audience and issuer, verify RS256, ES256 and EdDSA tokens against an identity provider's JWKS, peek at claims.7 views · 8 downloadsPASETO paseto.qwer · 1.0.0PASETO v4 tokens, the safer JWT: v4.local encrypted with a shared key and v4.public signed with Ed25519, expiry checked.7 views · 9 downloadsArgon2 passwords argon2.qwer · 1.0.0Argon2id password hashes with the memory and passes you choose, any Argon2 hash verified, its parameters read, old ones marked for a rehash, and the cost timed on this server.7 views · 6 downloadsBcrypt bcrypt.qwer · 1.0.0Sign in users whose bcrypt hashes came from PHP, Rails, Django or Laravel, then move them to Quillo's Argon2.8 views · 9 downloadsThread 0 posts
On Rate limits: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.