Security

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

#api#auth#abuse#middleware

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 error

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.

rate_limit_burst(key: str, rate: int, period_seconds: int, burst: int, cost: int) → anyor an error

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.

rate_limit_reset(key: str) → bool

Forgets 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.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/rate_limit.qwer$ curl -O https://quillolang.org/plugins/files/rate_limit.qwe

Then, in your program, all of it or only what you call:

main.qwe
use rate_limit.qweruse rate_limit, rate_limit_burst from rate_limit      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]

Example

rate_limit.qwe
// 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_limit.qwer
//! 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()}
Thread
76Useful? Like it, or add to its thread below.

Thread 0 posts

On Rate limits: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.