Start typing to search packages!
bouncer
By @karlobii
Roblox
Mirrored from WallyBouncer
A small module that wraps a RemoteEvent or RemoteFunction with:
- Per-player, per-remote token-bucket rate limiting (burst tolerant, not a blunt fixed window)
- Optional argument validation - a single predicate function, or an array of per-argument checkers that's drop-in compatible with
tcheckers - Violation reporting, not silent drops or auto-kicks - you decide what happens when a call looks bad
- Optional violation escalation - fire a callback once a player crosses N violations in a sliding window, for feeding into your own flag/ban system
It doesn't replace Knit, Comm, or rbx-net, it wraps whatever RemoteEvent/RemoteFunction you're already using, framework or not.
Install
[dependencies]
Bouncer = "karlobii/bouncer@^1"
Usage - RemoteEvent
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Bouncer = require(ReplicatedStorage.Packages.Bouncer)
local t = require(ReplicatedStorage.Packages.t)
local giveItemRemote = ReplicatedStorage.GiveItemRemote
local guard = Bouncer.new(giveItemRemote, {
rate = { max = 5, window = 1 }, -- 5 tokens refill per second
burst = 10, -- bucket can hold up to 10
validate = { t.string, t.number }, -- per-argument, t-compatible checkers
onViolation = function(player, reason, detail)
warn(("[Bouncer] %s violated %s (%s)"):format(player.Name, reason, detail or "n/a"))
-- log, soft-flag, escalate - Bouncer never kicks for you
end,
escalation = {
threshold = 5, -- 5 violations...
window = 10, -- ...within 10 seconds...
onEscalate = function(player, count)
warn(("[Bouncer] %s escalated: %d violations"):format(player.Name, count))
-- e.g. player:Kick(...), flag in a moderation datastore, etc.
end,
},
})
guard:Connect(function(player, itemId, qty)
-- Only reached for calls that passed rate limiting AND validation.
giveItem(player, itemId, qty)
end)
Bouncer forwards whatever arguments the client fires - remote:FireServer(itemId, qty) on the client arrives as guard:Connect(function(player, itemId, qty) ... end) on the server.
Validators
validate accepts two shapes:
- A function
(...) -> (boolean, string?)run against the whole argument list - full control, e.g. cross-argument checks (qty <= inventory.maxStack). - An array of per-argument checkers, each
(value) -> (boolean, string?)- this is exactly the signaturetcheckers already have (t.string,t.number,t.interface({...}), etc.), so you can droptcheckers straight in positionally:validate = { t.string, t.integer, t.optional(t.string) }. Extra args beyond the checker list pass through unchecked; missing args are checked againstnil.
Usage - RemoteFunction
local guard = Bouncer.new(myRemoteFunction, {
rate = { max = 5, window = 1 },
validate = { t.string },
rejectionResponse = { false, "rejected" }, -- what InvokeServer's caller receives on rejection
})
guard:OnInvoke(function(player, itemId)
return true, computeSomething(itemId)
end)
RemoteFunction.OnServerInvoke only supports one handler at a time, so :OnInvoke sets it directly (same restriction Roblox itself imposes - don't call :OnInvoke twice on the same guard). Rejected calls never reach your handler and instead return rejectionResponse (default: no values) to the caller, so client code that does local ok, data = remote:InvokeServer(...) can check ok either way.
API
Bouncer.new(remote: RemoteEvent | RemoteFunction, config: Config?) -> Bouncer
| config field | type | default | description |
|---|---|---|---|
rate | { max: number, window: number } | { max = 10, window = 1 } | tokens added per window seconds |
burst | number | rate.max | bucket capacity |
validate | (...) -> (boolean, string?) | { (value) -> (boolean, string?) } | none | function or per-argument (t-compatible) checker array |
onViolation | (player, reason, detail?) -> () | none | called on rejected calls only |
escalation | { threshold, window, onEscalate } | none | fires onEscalate(player, count) once threshold violations happen within window seconds; history resets after firing |
rejectionResponse | { any } | none (→ nil) | RemoteFunction only: values returned to the caller on rejection |
sweepInterval | number | 60 | seconds between stale-state sweeps |
staleAfter | number | 300 | idle seconds before a player's bucket/violation history is dropped |
reason is one of "RateLimited" | "InvalidPayload" | "NotAPlayer".
guard:Connect(handler: (player, ...) -> ()) -> RBXScriptConnection - RemoteEvent only
guard:OnInvoke(handler: (player, ...) -> ...any) - RemoteFunction only
guard:GetRemainingTokens(player) -> number
guard:ResetPlayer(player) - clears both rate-limit and violation history
guard:Destroy()
Design notes
- Token bucket, not fixed window - tolerates normal bursts (e.g. a UI double-click) without penalizing laggy-but-legitimate clients.
- Never auto-kicks - aggressive kicking causes false positives on lag/legit bursts; that decision is left to your
onViolation/escalation.onEscalatecallbacks. - Per-player AND per-remote - one abused remote can't starve traffic on your other remotes.
- Framework-agnostic - wraps a raw
RemoteEvent/RemoteFunction; use it standalone, inside Knit, or around Comm/Net. t-compatible by construction, not by dependency - Bouncer doesn't depend ontitself; any function matching(value) -> (boolean, string?)works,tcheckers just happen to already match that shape.
Github pages documentation coming soon.
Package Details
Install command (Click to copy)
Version
1.0.2
License
MIT
Safe for commercial use
Automated license review — not legal advice.
