If you’ve been working with RemoteEvents in Roblox, you’ve probably heard the golden rule: never trust the client. But what does that really mean, and how do you actually protect your game from exploiters? In this guide, we’ll explore why RemoteEvents are vulnerable, what exploiters can do, and most importantly, how to write secure server-side code that keeps your game safe.
Understanding the Problem: Why RemoteEvents Are Vulnerable
RemoteEvents are the bridge between client and server in Roblox. When a player clicks a button, picks up an item, or performs any action, you often use a RemoteEvent to communicate that action to the server. The problem? Exploiters can fire RemoteEvents too—with whatever arguments they want, as many times as they want.
Here’s a common mistake that intermediate scripters make:
-- Server Script (INSECURE!)
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local GiveMoneyEvent = ReplicatedStorage:WaitForChild("GiveMoneyEvent")
GiveMoneyEvent.OnServerEvent:Connect(function(player, amount)
local leaderstats = player:FindFirstChild("leaderstats")
local coins = leaderstats:FindFirstChild("Coins")
coins.Value = coins.Value + amount
end)
This code lets the client decide how much money to give themselves. An exploiter could fire this event with amount = 1000000 and instantly become rich. The server blindly trusts whatever the client sends—and that’s the vulnerability.
The Golden Rules of RemoteEvent Security
Before we dive into specific techniques, let’s establish the core principles:
- Never trust client data – Always validate every argument the client sends
- The server decides everything important – Money, damage, item ownership, etc. should be calculated server-side
- Clients send intentions, not outcomes – The client says “I want to buy this,” the server decides if they can and what happens
- Use sanity checks – Verify that requests make logical sense (cooldowns, distance, ownership, etc.)
Technique 1: Server-Side Validation
The most fundamental security technique is validating every piece of data the client sends. Let’s fix our money example:
-- Server Script (SECURE)
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local CollectCoinEvent = ReplicatedStorage:WaitForChild("CollectCoinEvent")
local COIN_VALUE = 10 -- Server decides the value
local COLLECT_DISTANCE = 10 -- Maximum distance to collect
CollectCoinEvent.OnServerEvent:Connect(function(player, coinInstance)
-- Validate the coin exists
if not coinInstance or not coinInstance:IsA("BasePart") then
return
end
-- Validate the coin is in the workspace where it should be
if not coinInstance:IsDescendantOf(workspace.Coins) then
return
end
-- Validate player is close enough
local character = player.Character
if not character then return end
local humanoidRootPart = character:FindFirstChild("HumanoidRootPart")
if not humanoidRootPart then return end
local distance = (humanoidRootPart.Position - coinInstance.Position).Magnitude
if distance > COLLECT_DISTANCE then
return -- Too far away
end
-- All checks passed! Give the reward
coinInstance:Destroy()
local leaderstats = player:FindFirstChild("leaderstats")
local coins = leaderstats:FindFirstChild("Coins")
coins.Value = coins.Value + COIN_VALUE
end)
Notice how the server validates everything: the coin exists, it’s in the right place, and the player is close enough. The server also decides the coin’s value—not the client.
Technique 2: Cooldown Protection
Exploiters often spam RemoteEvents hundreds of times per second. Implementing cooldowns prevents this abuse:
-- Server Script
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ShootEvent = ReplicatedStorage:WaitForChild("ShootEvent")
local cooldowns = {} -- Track last use time per player
local COOLDOWN_TIME = 0.5 -- 0.5 seconds between shots
ShootEvent.OnServerEvent:Connect(function(player)
local currentTime = tick()
local lastUse = cooldowns[player.UserId] or 0
-- Check if enough time has passed
if currentTime - lastUse < COOLDOWN_TIME then
return -- Still on cooldown
end
-- Update the cooldown
cooldowns[player.UserId] = currentTime
-- Process the shot
-- ... your shooting logic here ...
end)
-- Clean up when player leaves
game.Players.PlayerRemoving:Connect(function(player)
cooldowns[player.UserId] = nil
end)
This ensures players can't fire faster than intended, even with exploits. The server enforces the timing, not the client.
Technique 3: Ownership Verification
Always verify that the player actually owns or has access to the items they're trying to manipulate:
-- Server Script
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local EquipToolEvent = ReplicatedStorage:WaitForChild("EquipToolEvent")
local playerInventories = {} -- Store what each player owns
EquipToolEvent.OnServerEvent:Connect(function(player, toolName)
-- Validate the tool name is a string
if type(toolName) ~= "string" then
return
end
-- Check if player owns this tool
local playerInventory = playerInventories[player.UserId]
if not playerInventory or not playerInventory[toolName] then
warn(player.Name .. " tried to equip a tool they don't own!")
return
end
-- Player owns it, proceed with equipping
local tool = game.ServerStorage.Tools:FindFirstChild(toolName)
if tool then
local toolClone = tool:Clone()
toolClone.Parent = player.Backpack
end
end)
The server maintains its own record of what players own. Even if an exploiter sends a request for a premium tool, the server checks ownership first.
Technique 4: Type Checking and Range Validation
Always validate that arguments are the correct type and within acceptable ranges:
-- Server Script
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local CustomizeColorEvent = ReplicatedStorage:WaitForChild("CustomizeColorEvent")
CustomizeColorEvent.OnServerEvent:Connect(function(player, color)
-- Type check: ensure it's a Color3
if typeof(color) ~= "Color3" then
return
end
-- Range check: ensure RGB values are valid (0-1)
if color.R < 0 or color.R > 1 or
color.G < 0 or color.G > 1 or
color.B < 0 or color.B > 1 then
return
end
-- Apply the color
local character = player.Character
if character then
local bodyColors = character:FindFirstChildOfClass("BodyColors")
if bodyColors then
bodyColors.HeadColor3 = color
end
end
end)
Technique 5: Server-Side Raycasting for Combat
Never let clients tell the server who they hit. Instead, the server should verify hits with its own raycasts:
-- Server Script
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ShootEvent = ReplicatedStorage:WaitForChild("ShootEvent")
local MAX_RANGE = 300
local DAMAGE = 25
ShootEvent.OnServerEvent:Connect(function(player, targetPosition)
local character = player.Character
if not character then return end
local tool = character:FindFirstChildOfClass("Tool")
if not tool or tool.Name ~= "Gun" then return end
local gunBarrel = tool:FindFirstChild("Barrel")
if not gunBarrel then return end
-- Verify the target position is within range
local distance = (gunBarrel.Position - targetPosition).Magnitude
if distance > MAX_RANGE then
return
end
-- Server performs its own raycast
local rayOrigin = gunBarrel.Position
local rayDirection = (targetPosition - rayOrigin).Unit * MAX_RANGE
local raycastParams = RaycastParams.new()
raycastParams.FilterDescendantsInstances = {character}
raycastParams.FilterType = Enum.RaycastFilterType.Blacklist
local result = workspace:Raycast(rayOrigin, rayDirection, raycastParams)
if result then
local hitPart = result.Instance
local hitCharacter = hitPart:FindFirstAncestorOfClass("Model")
if hitCharacter then
local humanoid = hitCharacter:FindFirstChildOfClass("Humanoid")
if humanoid then
humanoid:TakeDamage(DAMAGE)
end
end
end
end)
The client sends where they aimed, but the server independently verifies what was actually hit. This prevents exploiters from claiming they hit players through walls or from impossible distances.
Additional Security Best Practices
Use Server-Side Logging
Track suspicious behavior to identify potential exploiters:
local suspiciousActivity = {}
-- Inside your RemoteEvent handler
if somethingInvalid then
suspiciousActivity[player.UserId] = (suspiciousActivity[player.UserId] or 0) + 1
if suspiciousActivity[player.UserId] > 10 then
player:Kick("Suspicious activity detected")
end
return
end
Separate Important Events
Don't use one RemoteEvent for everything. Separate critical actions (purchases, admin commands) from regular gameplay to make security code cleaner and more maintainable.
Keep Critical Code Server-Only
Store important scripts in ServerScriptService, not ReplicatedStorage. Exploiters can read any code that replicates to the client.
Recap and Next Steps
Securing RemoteEvents comes down to one principle: never trust the client. Always validate data types, check ownership, enforce cooldowns, verify distances, and let the server make all important decisions. The client should only communicate intentions—the server decides outcomes.
Remember these key points:
- Validate every argument clients send
- Implement cooldowns to prevent spam
- Verify ownership and permissions server-side
- Use server-side raycasting for combat
- Log suspicious activity
Ready to level up your security knowledge? Next, explore server-side data validation patterns and learn about DataStore security to protect player data. You might also want to study anti-cheat systems that detect speed hacks and other exploits automatically. Keep building, keep learning, and keep your games secure!