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:

  1. Enumerate: for _, remote in ReplicatedStorage:GetDescendants() — you cannot validate remotes you forgot you made.
  2. 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.
  3. Arguments: every one type-checked, range-checked, ownership-checked?
  4. Rate limit: is there a server-side cooldown or burst limit?
  5. State: distance, alive, equipped, round-active checks applied?
  6. Numbers: does any client-sent number reach damage, currency, or speed math unsanitized?
  7. 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

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.

Related pages