Async and Events
RateLimiter
Token buckets, sliding and fixed windows, leaky buckets, cooldowns, debounce and throttle, all keyed per player or per anything.
Getting it
local RateLimiter = Engine:GetPackage("RateLimiter")Overview
RateLimiter answers one question: "may this happen right now?" Every limiter tracks usage per key. Pass a player's UserId as the key and each player gets their own budget; leave the key out and all calls share one budget.
local RateLimiter = Engine:GetPackage("RateLimiter")
-- 5 requests per second on average, bursts of up to 10
local limiter = RateLimiter.TokenBucket(5, 10)
remote.OnServerEvent:Connect(function(player, ...)
local allowed, retryAfter = limiter:Allow(player.UserId)
if not allowed then
warn(player.Name, "is sending too fast, retry in", retryAfter)
return
end
handleRequest(player, ...)
end)
Players.PlayerRemoving:Connect(function(player)
limiter:Reset(player.UserId)
end)Which limiter to pick
| Limiter | Behaviour | Good for |
|---|---|---|
| TokenBucket | Refills steadily, allows short bursts. | Remotes, chat, most player actions. |
| SlidingWindow | Exact count over the last N seconds. | Strict caps such as "3 reports per minute". |
| SlidingWindowCounter | Close estimate of a sliding window using two counters. | High-volume keys where memory matters. |
| FixedWindow | Count resets at fixed boundaries. | "100 per hour" quotas, simple to explain to players. |
| LeakyBucket | Drains at a steady rate, no bursts above capacity. | Smoothing a steady flow. |
| Cooldown | One use, then wait. | Abilities, emotes, buttons. |
| Group | Several limiters at once, all must allow. | Combining a burst limit with an hourly quota. |
| Debounce / Throttle | Wrap a function instead of asking. | Search boxes, resize handlers, saving. |
Shared limiter API
All limiters (everything except Debounce and Throttle) have these members.
Allow
limiter:Allow(key: Key?, cost: number?): (boolean, number?)Tries to spend cost (default 1) for key. Returns true when allowed. When not allowed it returns false plus the number of seconds after which the same call would succeed, and fires Limited.
Refund
limiter:Refund(key: Key?, cost: number?)Gives back cost that was spent, for example when the action you allowed failed for another reason.
Reset
limiter:Reset(key: Key?)Forgets the state of one key. Without a key it forgets every key.
Prune
limiter:Prune(maxIdleSeconds: number?): numberDrops keys that have not been used for maxIdleSeconds (default from configuration) and returns how many were dropped. Limiters also prune themselves every few thousand calls, so long-running servers do not collect stale keys.
Limited
limiter.Limited: Signal<(key: Key?, cost: number)>Fires every time a call is refused. Handy for logging abuse.
Limiters
TokenBucket
RateLimiter.TokenBucket(ratePerSec: number, burst: number, clock: Clock?): TokenBucketLimiterEach key holds up to burst tokens and gains ratePerSec tokens per second. A call spends cost tokens.
| Extra member | Description |
|---|---|
Rate: number | Tokens per second. Can be changed at any time; must be positive. |
Burst: number | Bucket size. Can be changed at any time; must be positive. |
:Available(key?): number | Tokens the key has right now. |
SlidingWindow
RateLimiter.SlidingWindow(limit: number, windowSec: number, clock: Clock?): SlidingWindowLimiterAllows at most limit cost within any windowSec long stretch. It remembers every call in the window, so it is exact.
| Extra member | Description |
|---|---|
Limit: number / Window: number | Settable. |
:Count(key?): number | Cost used inside the current window. |
SlidingWindowCounter
RateLimiter.SlidingWindowCounter(limit: number, windowSec: number, clock: Clock?): SlidingWindowCounterLimiterLike SlidingWindow, but stores only the count of the current and the previous window and weighs them, so memory use is constant per key. Same extra members as SlidingWindow.
FixedWindow
RateLimiter.FixedWindow(limit: number, windowSec: number, clock: Clock?): FixedWindowLimiterAllows limit cost per window; the count resets when the clock crosses into the next window.
| Extra member | Description |
|---|---|
Limit: number / Window: number | Settable. |
:Remaining(key?): number | Cost left in the current window. |
:ResetsIn(key?): number | Seconds until the window resets. |
LeakyBucket
RateLimiter.LeakyBucket(ratePerSec: number, capacity: number, clock: Clock?): LeakyBucketLimiterEach call pours cost into a bucket of size capacity that drains at ratePerSec. A call that would overflow is refused.
| Extra member | Description |
|---|---|
Rate: number / Capacity: number | Settable. |
:Level(key?): number | How full the bucket is. |
Cooldown
RateLimiter.Cooldown(duration: number, clock: Clock?): CooldownGateAllows one call, then refuses until duration seconds have passed. cost is ignored. :Refund() ends the cooldown early.
local dash = RateLimiter.Cooldown(3)
local function tryDash(player: Player)
if dash:Allow(player.UserId) then
performDash(player)
end
end| Extra member | Description |
|---|---|
Duration: number | Settable. |
:Ready(key?): boolean | Whether the next call would be allowed. |
:TimeLeft(key?): number | Seconds until ready, 0 when ready. |
Group
RateLimiter.Group(limiters: { Limiter }): GroupLimiterAsks each limiter in order. The call is allowed only when all of them allow it; if one refuses, the ones that already allowed are refunded and the longest retryAfter is returned. :GetLimiters() returns a copy of the list.
local chat = RateLimiter.Group({
RateLimiter.TokenBucket(1, 5), -- short bursts
RateLimiter.FixedWindow(200, 3600), -- and at most 200 messages an hour
})Function wrappers
Debounce
RateLimiter.Debounce(fn: (...any) -> (), t: number, options: DebounceOptions?): ((...any) -> (), DebounceControl)Returns a wrapped function. Calling it repeatedly only calls fn once things have been quiet for t seconds, with the arguments of the last call. With Leading = true, fn also runs on the first call of a burst. Defaults: Leading = false, Trailing = true.
local search, control = RateLimiter.Debounce(function(text)
runSearch(text)
end, 0.3)
textBox:GetPropertyChangedSignal("Text"):Connect(function()
search(textBox.Text)
end)Throttle
RateLimiter.Throttle(fn: (...any) -> (), t: number, options: ThrottleOptions?): ((...any) -> (), ThrottleControl)Returns a wrapped function that calls fn at most once every t seconds. With Leading (default true) the first call runs at once; with Trailing (default true) the last call made during the wait runs when the wait ends. SchedulerService:Throttle is the same thing expressed in calls per second.
Control object
Both wrappers also return a control object.
| Member | Description |
|---|---|
Pending: boolean | Whether a call is waiting (Debounce) or the wrapper is inside its wait (Throttle). |
:Cancel() | Drops anything waiting without calling fn. |
:Flush() | Calls fn right now with the waiting arguments, if any, and ends the wait. |
Options for both: { Leading: boolean?, Trailing: boolean?, clock: Clock? }.
Configuration
Add a RateLimiter module to your Configuration folder to change these defaults. See Configuration.
DefaultConfiguration/Packages/RateLimiter7 settings| Setting | Default | What it does |
|---|---|---|
| DefaultCost | 1 | Cost :Allow()/:Refund() charge when none is passed. |
| DefaultPruneIdleSeconds | 300 | Idle seconds after which :Prune() drops a key when none is passed. |
| AutoPruneEvery | 2000 | :Allow() calls between automatic prune sweeps. |
| DefaultDebounceLeading | false | Whether a Debounce fires on the first call when options.Leading is omitted. |
| DefaultDebounceTrailing | true | Whether a Debounce fires after the last call when options.Trailing is omitted. |
| DefaultThrottleLeading | true | Whether a Throttle fires on the first call when options.Leading is omitted. |
| DefaultThrottleTrailing | true | Whether a Throttle fires once more at the end of a window when options.Trailing is omitted. |
Types
export type Key = string | number
export type Limiter
export type TokenBucketLimiter
export type SlidingWindowLimiter
export type SlidingWindowCounterLimiter
export type FixedWindowLimiter
export type LeakyBucketLimiter
export type CooldownGate
export type GroupLimiter
export type DebounceOptions
export type DebounceControl
export type ThrottleOptions
export type ThrottleControl