Skip to content

Limit

local Limit = require("@game/ReplicatedStorage/Twill").Limit
local allowance = Limit.PerPlayer(10)
if not allowance:Take(player) then
return
end

A bucket refills at its rate and never holds more than its burst. It starts full, so the first call always passes.

A rate below one is a cooldown. Limit.new(0.25) permits one call every four seconds.

The burst never falls below one token however slow the refill, so a slow bucket still admits a single call rather than none. A refused call spends nothing, so being turned away does not push a caller’s own recovery further out.

Take credits what the time since the last call earned back, then spends. There is no timer and no per-bucket work between calls.

PerPlayer keeps a separate bucket for every player, and a separate one for every key of theirs.

local allowance = Limit.PerPlayer(4)
allowance:Take(player, "BuyItem")
allowance:Take(player, "Emote")

Both spend from their own bucket at the same rate. Leaving the key out uses one shared bucket per player.

A player’s buckets are created the moment they are first needed and forgotten when that player leaves. There is no window in which a caller is unmetered, and nothing accumulates for players who are gone.

Logging once per refusal makes the log the flood. Throttle counts refusals instead, and answers with the count at most once per interval. Writing the line stays with the caller.

local speak = Limit.Throttle(5)
local refused = speak(player, remoteName)
if refused then
logger:Warn(`{player.Name} was refused {refused} time(s)`)
end

The count covers everything since the last time it spoke, including the call that is speaking now. Between reports it answers nil.

Records are keyed the same way allowances are, so a player and a remote each get their own quiet period. A player’s records are forgotten when they leave.

[Server] | [Client]

Creates one allowance that refills at the given rate.

function Limit.new(rate: number, burst: number?): Bucket

Parameters

NameTypeDescription
ratenumberUnits earned back per second. Must be above zero.
burstnumber?The most it will ever hold. The rate itself when left out, and never less than one.

Returns

Bucket - An allowance for one caller.

Throws when the rate is not a number above zero.

Not keyed by anything. Use it for a global budget: an outward call, a shared resource, a server-wide announcement.

[Server] | [Client]

Creates an allowance per player, forgotten when that player leaves.

function Limit.PerPlayer(rate: number, burst: number?): Allowance

Returns

Allowance - An allowance covering every player.

Throws when the rate is not a number above zero.

[Server] | [Client]

Creates a reporter that speaks at most once per interval and counts the rest.

function Limit.Throttle(interval: number): Throttle

Parameters

NameTypeDescription
intervalnumberSeconds of quiet between one report and the next. Must be above zero.

Returns

Throttle - A function answering how many went unreported, or nil to stay quiet.

Throws when the interval is not a number above zero.

[Server] | [Client]

Spends from the allowance, crediting what the time since the last call earned back.

function Bucket:Take(amount: number?): boolean

Returns

boolean - false when there was not enough left to spend. One unit when the amount is left out.

A cost larger than the burst can never be met.

[Server] | [Client]

Spends one unit of a player’s allowance, with each key kept apart.

function Allowance:Take(player: Player, key: any?): boolean

Returns

boolean - false when that player has nothing left to spend on that key.

[Server] | [Client]

type Throttle = (key: any, subKey: any?) -> number?

Answers the number of calls since it last spoke, or nil while it is staying quiet.

CallerWhat it meters
NetCalls per player per remote, and total bytes per player.
NetRefusal reports, once per five seconds per player and remote.
StoreReports of a peer sending something unreadable.