Server-Side Validation for Roblox RemoteEvents
Last updated:
Quick answer: Exploiters can fire any RemoteEvent with whatever arguments they like, so the server must type-check, range-check, and rate-limit every call and keep damage, currency, and inventory decisions server-side.
Everything a client sends to a RemoteEvent can be forged: an exploiter can fire any remote, with any arguments, as often as they like — so the server must treat every OnServerEvent call as a claim to be verified, not an order to obey. Server-side validation means checking types and ranges, confirming ownership and game state, enforcing rate limits, and keeping every gameplay decision (damage, currency, inventory) on the server where the exploiter cannot touch it.
What is the trust boundary in Roblox?
Roblox runs your game in two places: the server, which owns the game world, and one client per player, which runs LocalScripts and sends input. The exploiter fully controls their client — they can run arbitrary Luau, skip your UI entirely, and call FireServer on any RemoteEvent with hand-crafted arguments. Nothing that arrives OnServerEvent is guaranteed to come from your code.
The trust rule is simple: clients express intent, servers decide outcomes. A legitimate client says "the player clicked the buy button for the Sword"; the server decides whether the purchase is valid, what it costs, and whether the player can afford it. This is the same principle behind leaderstats — the numbers on the leaderboard are copies of server state, not values the client reports.
How do I validate RemoteEvent arguments?
Every argument needs three checks: it is the right type, it is within a sane range, and the player actually owns or may use what it refers to. A shop RemoteEvent is the classic example:
local shopCatalog = {
Sword = { price = 100 },
Shield = { price = 150 },
Potion = { price = 25 },
}
local function isPositiveInteger(value)
return type(value) == "number"
and value % 1 == 0
and value > 0
and value <= 10
end
buyItem.OnServerEvent:Connect(function(player, itemId, count)
-- 1. type and range checks on every argument
if type(itemId) ~= "string" or #itemId > 64 then
return
end
if not isPositiveInteger(count) then
return
end
-- 2. the SERVER catalog decides what exists, not the client
local item = shopCatalog[itemId]
if not item then
warn(player.Name .. " requested unknown item: " .. itemId)
return
end
-- 3. price math happens here, never on the client
local total = item.price * count
local coins = player:GetAttribute("Coins") or 0
if coins < total then
return
end
player:SetAttribute("Coins", coins - total)
giveItem(player, itemId, count)
end)
Note what never crosses the wire: the price, the discount, the resulting balance. The client sends only which item and how many. Look up itemId in a server-owned table instead of trusting it blindly — indexing with an unchecked string is both a crash (nil) and a potential exploit (attacker-chosen keys). The same three checks apply to every remote, and the RemoteEvents guide covers the wiring itself if you need the basics first.
Where should rate limits and cooldowns live?
On the server — always. A cooldown implemented in a LocalScript is a suggestion, not a limit: the exploiter never runs your LocalScript at all. Track the last accepted request per player in a server table with os.clock(), drop calls that arrive too fast, and clear the entry when the player leaves:
local Players = game:GetService("Players")
local lastRequest = {}
local COOLDOWN_SECONDS = 0.5
local function isRateLimited(player)
local now = os.clock()
local last = lastRequest[player]
if last and now - last < COOLDOWN_SECONDS then
return true -- too fast, silently drop
end
lastRequest[player] = now
return false
end
Players.PlayerRemoving:Connect(function(player)
lastRequest[player] = nil -- no leaks
end)
buyItem.OnServerEvent:Connect(function(player, itemId, count)
if isRateLimited(player) then
return
end
-- then the argument validation from the previous section
end)
Match the limit to the action: one purchase per half-second, one attack per swing time, one chat command per second. For abusers, add a burst counter (allow N requests per window, ban-ish warn beyond it) rather than only a per-call delay. And remember cleanup — every per-player table needs a PlayerRemoving handler or it grows forever.
What sanity checks should the server make?
Valid arguments are not enough; the request must be possible in the game world. Before applying an effect, ask what the player's character could actually do right now:
local MAX_HIT_DISTANCE = 20
attackRemote.OnServerEvent:Connect(function(player, target)
local character = player.Character
local root = character and character:FindFirstChild("HumanoidRootPart")
local humanoid = character and character:FindFirstChildOfClass("Humanoid")
local targetRoot = target and target:FindFirstChild("HumanoidRootPart")
if not root or not humanoid or not targetRoot then
return
end
-- distance sanity: a sword cannot hit across the map
if (root.Position - targetRoot.Position).Magnitude > MAX_HIT_DISTANCE then
warn(player.Name .. " attempted an impossible hit")
return
end
-- state sanity: dead players swing nothing
if humanoid.Health <= 0 then
return
end
-- ownership sanity: the tool must actually be equipped
local tool = character:FindFirstChildOfClass("Tool")
if not tool or tool.Name ~= "Sword" then
return
end
applyDamage(target, tool:GetAttribute("Damage") or 10)
end)
The three recurring sanity checks are distance (is the target in range?), ownership (is the tool equipped? does the player own the gamepass?), and state (alive, not stunned, round still active?). Check typeof(target) == "Instance" and target:IsA("Model") before indexing, since a forged remote can pass a number where you expect a character model.
How do I keep damage, currency and inventory server-authoritative?
Move the computation to the server and let the client only request. Damage comes from the tool's stats, not from a client-sent number. Currency changes happen in one server module that owns the balance; clients display it. Inventory lives in a server-side table that is persisted through the DataStore guide flow — never rebuilt from what a client claims to have.
A practical structure is a single server module per system:
-- ServerScriptService/CurrencyService (ModuleScript)
local CurrencyService = {}
function CurrencyService.add(player, amount)
assert(type(amount) == "number" and amount >= 0 and amount <= 10000)
local coins = player:GetAttribute("Coins") or 0
player:SetAttribute("Coins", coins + amount)
end
function CurrencyService.spend(player, amount)
local coins = player:GetAttribute("Coins") or 0
if amount < 0 or coins < amount then
return false
end
player:SetAttribute("Coins", coins - amount)
return true
end
return CurrencyService
Every remote funnels through these functions, so there is exactly one place where money changes — and exactly one place to audit. If a remote can mutate coins without going through the service, that is the bug.
How do I audit my remotes before publishing?
Walk every RemoteEvent and RemoteFunction in your game and run this checklist against its handler:
- Enumerate:
for _, remote in ReplicatedStorage:GetDescendants()— you cannot validate remotes you forgot you made. - Worst case: what is the most damage one forged call can do? Delete all player progress? Crash the server? If the answer is bad, the handler needs more checks.
- Arguments: every one type-checked, range-checked, ownership-checked?
- Rate limit: is there a server-side cooldown or burst limit?
- State: distance, alive, equipped, round-active checks applied?
- Numbers: does any client-sent number reach damage, currency, or speed math unsanitized?
- Test it yourself: from the Studio command bar while playing, fire your own remote with garbage — wrong types, negative counts, huge distances — and watch what the server does.
RemoteFunctions deserve extra suspicion: their return values leak server state, and a handler that errors on bad input can be spammed to spam error logs.
Common mistakes in remote validation
- Trusting client-computed numbers. A remote that says "deal 500 damage" or "add 1000 coins" is a cheat button. Send intent, compute results on the server.
- Rate limiting on the client only. UI debounce is fine for feel, but the enforceable limit lives in the server handler.
- Indexing tables with unchecked strings.
shop[itemId]with an attacker-chosenitemIdeither crashes on nil or hits keys you never intended. Validate, then look up in a server-owned catalog. - Forgetting PlayerRemoving cleanup. Per-player cooldown tables that never shrink are a slow memory leak. Key them per player and clear each entry when the player leaves.
- Checking once, applying many. A remote that fires ten projectiles must validate all ten, not just the first argument batch.
- Treating the UI as a security boundary. Hiding a button does not remove the remote. Everything a visible UI can do, an exploiter can do faster.
Learn Luau by doing
Validation code only teaches its value the day you fire your own remote with garbage arguments and watch the server shrug. On Luablox you can practice Luau interactively through /theory, pressure-test these patterns with drills on /problems, and follow structured Luablox lessons that build from RemoteEvents to full server-authoritative gameplay systems.
Frequently asked questions
Can exploiters call my RemoteEvent?
Yes. Any exploiter can fire any RemoteEvent in the game with arbitrary arguments and at any frequency, without ever running your LocalScripts. The server must validate every OnServerEvent call: type-check the arguments, enforce a cooldown, and verify the request is possible before applying any effect.
Where should the cooldown go, client or server?
Server, always. A cooldown inside a LocalScript only polices honest players, because exploiters never run that script. Track the last accepted request per player in a server table with os.clock(), drop calls that arrive too fast, and clear the entry in PlayerRemoving.
How do I stop players from giving themselves money?
Never accept amounts from the client. The client should send only intent such as "buy the Sword", while the server looks up the price in its own catalog, checks the balance, and performs the subtraction in one server-owned currency module. If no remote can add coins outside that module, there is nothing for an exploiter to abuse.
Is a RemoteFunction safer than a RemoteEvent?
No. RemoteFunctions have the same trust model: the exploiter can invoke them with any arguments as often as they like. They also return server data to the caller, so validate inputs just as strictly and avoid returning information the client should not have.
Do simple remotes really need distance and state checks?
If the remote changes the game world, yes. An attack or pickup remote without a distance check lets exploiters hit or collect from anywhere on the map. Check that the target is in range, the player is alive, and the required tool is equipped before applying the effect.
Explore Luau networking concepts in Roblox Studio, covering RemoteEvents, RemoteFunctions, and secure client-server replication.