Forest Logo
search
package_2

bouncer

By @karlobii

Roblox

Mirrored from Wally

Bouncer

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 t checkers
  • 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 signature t checkers already have (t.string, t.number, t.interface({...}), etc.), so you can drop t checkers 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 against nil.

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 fieldtypedefaultdescription
rate{ max: number, window: number }{ max = 10, window = 1 }tokens added per window seconds
burstnumberrate.maxbucket capacity
validate(...) -> (boolean, string?) | { (value) -> (boolean, string?) }nonefunction or per-argument (t-compatible) checker array
onViolation(player, reason, detail?) -> ()nonecalled on rejected calls only
escalation{ threshold, window, onEscalate }nonefires 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
sweepIntervalnumber60seconds between stale-state sweeps
staleAfternumber300idle 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.onEscalate callbacks.
  • 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 on t itself; any function matching (value) -> (boolean, string?) works, t checkers 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

check_circle

Safe for commercial use

Automated license review — not legal advice.