RateLimiter

RateLimiter protects your remotes from spam and abuse using a sliding window: it counts how many calls a player made in the last N seconds and rejects the request once that count crosses the limit. It's used as Middleware either globally or on a specific Service.

How it works internally

Player sends a request

    ├── Player banned? > false (warn + seconds remaining)

    ├── Prune timestamps outside the window
    ├── count >= maxCalls?
    │       ├── Penalties enabled > applyPenalty()
    │       │       ├── violations >= banThreshold  > kick + temporary ban
    │       │       ├── violations >= kickThreshold > kick
    │       │       └── violations >= warnThreshold > console warn
    │       └── Penalties disabled > console warn (with cooldown)
    │       └── return false

    └── Timestamp recorded > return true

Available shortcuts

Function Description Penalties
RateLimiter.perPlayer(max, window, label) Per-player individual limit ❌ off by default
RateLimiter.global(max, window, label) Limit shared across every player ❌ off by default
RateLimiter.strict(max, window, label) Per-player limit + penalties enabled (low thresholds)

Basic usage

-- > // Server: inside Owl.Start(), applied to every remote
Owl.Start({
    GlobalMiddleware = {
        Inbound = {
            Owl.Util.RateLimiter.perPlayer(30, 1, "Global"),
        },
    },
})
 
-- > // On a specific Service (stacked on top of the global one)
local ShopService = Owl.CreateService({
    Name = "ShopService",
    Middleware = {
        Inbound = {
            Owl.Util.RateLimiter.perPlayer(5, 1, "ShopService"),
        },
    },
    Client = { --[[ ... ]] },
})

Progressive penalties (opt-in)

Penalties are disabled by default. Turn them on through the penalty field:

Owl.Util.RateLimiter.perPlayer(10, 1, "CombatService", {
    enabled = true,
    warnThreshold = 3, -- > // warn after 3 violations
    kickThreshold = 8, -- > // kick after 8 violations
    banThreshold = 15, -- > // temporary ban after 15 violations
    banDuration = 60, -- > // ban duration, in seconds
    kickMessage = "You were kicked for spamming.",
    banMessage  = "You were banned for 60s for spamming.",
})

Violations are cumulative over time, not just within a single window. A player spamming in short bursts still eventually reaches the thresholds resetting the window doesn't reset the violation count.

The strict shortcut

For critical remotes (purchases, save data, combat), strict enables penalties with low thresholds out of the box:

local PurchaseService = Owl.CreateService({
    Name = "PurchaseService",
    Middleware = {
        Inbound = {
            -- > // Max 3 purchases/s, kick after 5 violations, ban for 60s after 10
            Owl.Util.RateLimiter.strict(3, 1, "PurchaseService"),
        },
    },
    Client = { --[[ ... ]] },
})

Full configuration

For anything the shortcuts above don't cover, construct a RateLimiter directly:

RateLimiter.new({
    maxCalls = 20, -- > // max calls allowed within the window
    window = 1, -- > // window duration, in seconds (default: 1)
    global = false, -- > // true = limit shared across every player
    warnCooldown = 5, -- > // seconds between two console warns (default: 5)
    label = "MyRemote", -- > // name shown in warn output
    penalty = {
        enabled = true,
        warnThreshold = 3,
        kickThreshold = 10,
        banThreshold  = 20,
        banDuration = 30,
        kickMessage = "Kicked for spamming.",
        banMessage = "Banned for 30s for spamming.",
    },
})

Where to go next

  • Middleware - how RateLimiter slots into the global vs per-Service cascade.
  • TypeChecker - the other built-in middleware, validating argument shapes instead of call frequency.