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.
Report Issue