How to rate limit RemoteEvents in Roblox
A remote that spawns parts, plays effects or calls data stores can be fired far faster than any button. How to add a per-player cooldown on the server.
What it looks like
A handler does something costly or visible to everyone each time it's fired, and nothing limits how often one player can fire it. Typical work: creating parts or models, cloning, playing effects for all players, data store calls, pathfinding or raycasts over large areas.
Luau
remote.OnServerEvent:Connect(function(player) local pet = Instance.new("Part") pet.Parent = workspace end)
How to detect it
- For each handler, ask what one call costs and whether it leaves anything behind: new instances, table entries, scheduled threads, network traffic to other players or requests to Roblox services.
- Look for a per-player cooldown (a table keyed by player storing
os.clock()), a cap on how many things a player can own, or a "busy" flag that blocks repeats. - Check that per-player tables are cleared in
Players.PlayerRemoving, or they grow for the life of the server. - Cooldowns on the client don't count. An exploiter can fire the remote directly.
What an exploiter could do
They can fire the remote as fast as their client allows. If each call creates something or does heavy work, the server slows down for everyone in it, and things that are never cleaned up pile up until the server is unplayable.
How to fix it
- Keep the last time each player used the remote and ignore calls inside the cooldown. Use
os.clock()on the server. - Cap what one player can own at once, for example one pet each, and clean up the old one before making a new one.
- Clear per-player entries in
Players.PlayerRemoving.
Vulnerable example
Vulnerable
local ReplicatedStorage = game:GetService("ReplicatedStorage") ReplicatedStorage.Remotes.SpawnPet.OnServerEvent:Connect(function(player: Player) local character = player.Character local root = character and character:FindFirstChild("HumanoidRootPart") if not root or not root:IsA("BasePart") then return end local pet = Instance.new("Part") pet.Name = player.Name .. "Pet" pet.Size = Vector3.new(1, 1, 1) pet.CFrame = root.CFrame * CFrame.new(2, 1, 0) pet.Parent = workspace end)
Fixed example
Fixed
local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local cooldown = 3 local lastSpawn: { [Player]: number } = {} local pets: { [Player]: Part } = {} ReplicatedStorage.Remotes.SpawnPet.OnServerEvent:Connect(function(player: Player) local now = os.clock() if now - (lastSpawn[player] or 0) < cooldown then return end local character = player.Character local root = character and character:FindFirstChild("HumanoidRootPart") if not root or not root:IsA("BasePart") then return end lastSpawn[player] = now if pets[player] then pets[player]:Destroy() end local pet = Instance.new("Part") pet.Name = player.Name .. "Pet" pet.Size = Vector3.new(1, 1, 1) pet.CFrame = root.CFrame * CFrame.new(2, 1, 0) pet.Parent = workspace pets[player] = pet end) Players.PlayerRemoving:Connect(function(player) if pets[player] then pets[player]:Destroy() end pets[player] = nil lastSpawn[player] = nil end)
Common false positives
- Handlers that only read or change a small value on the firing player's own data, with nothing created and nothing sent to others, are cheap to repeat. A missing rate limit there is not worth a finding.
- A "busy" flag or pending-request table that blocks a second call until the first finishes already limits the rate.
- Cooldowns enforced inside a shared helper module count. Read it before reporting.