Recipes · 5 of 6
Rate limiting — atomic counters with TTL
Per-key counters that auto-expire. Pair OxiScript with a TTL index on the bucket key for free cleanup.
Schema
rate_buckets: {key, count, window_start, ttl_at}
# TTL index on ttl_at via create_ttl_index
check_rate
proc check_rate(key, max_per_window, window_seconds) {
let bucket = find_one("rate_buckets", {key: key})
if bucket == null {
insert("rate_buckets", {
key: key, count: 1,
window_start: "now",
ttl_at: "now+window_seconds"
})
return {allowed: true, count: 1, limit: max_per_window}
}
if bucket.count >= max_per_window {
return {allowed: false, count: bucket.count, limit: max_per_window}
}
update("rate_buckets", {key: key}, {$inc: {count: 1}})
return {allowed: true, count: bucket.count + 1, limit: max_per_window}
}
guard_with_rate_limit
proc send_email(user_id, recipient, subject, body) {
let key = "email:" // would concat user_id in real code
let r = check_rate({key: key, max_per_window: 10, window_seconds: 3600})
if !r.allowed {
abort "rate limit exceeded"
}
insert("emails", {
user_id: user_id, recipient: recipient,
subject: subject, body: body, status: "queued"
})
return {ok: true, sent_in_window: r.count}
}
Rate-limited login
proc login_attempt(email, password_hash) {
let key = "login:"
let r = check_rate({key: key, max_per_window: 5, window_seconds: 300})
if !r.allowed {
insert("audit_log", {action: "login_blocked", target: email})
abort "too many attempts — try again later"
}
let user = find_one("users", {email: email})
if user == null { abort "invalid credentials" }
if user.password_hash != password_hash { abort "invalid credentials" }
return {ok: true, user_id: user._id}
}
Per-IP and per-user combined
proc submit_form(user_id, ip, payload) {
let r1 = check_rate({key: "submit:user:", max_per_window: 20, window_seconds: 60})
let r2 = check_rate({key: "submit:ip:", max_per_window: 100, window_seconds: 60})
if !r1.allowed || !r2.allowed {
abort "rate limit"
}
insert("submissions", {user_id: user_id, ip: ip, payload: payload})
return {ok: true}
}
Set up the TTL index once:
{"cmd": "create_ttl_index", "collection": "rate_buckets", "field": "ttl_at", "expireAfterSeconds": 0} — the engine will auto-purge expired buckets.