Why server anti-cheat shouldn't ban on one reading
Lag, falls, flings and the game's own teleports trip movement checks. How to move players back instead of kicking, and allow for your own movement.
What it looks like
A server-side speed, fly or teleport check kicks or bans a player the first time one measurement looks wrong. It doesn't allow for lag, the game's own teleports, vehicles, launch pads, respawns or being flung by physics.
Luau
if distance > humanoid.WalkSpeed * delta then player:Kick("Exploiting") end
How to detect it
- Find server code that measures a character's movement: distance between position samples,
AssemblyLinearVelocity, time in the air, or height above the floor. - Look at what happens when the check fails.
Kick,BanAsyncor a saved ban after a single failed reading is a finding. Moving the player back, or acting only after several failures in a short window, is the safer shape. - Check that the game's own movement is allowed for: server teleports, respawns, seats and vehicles, speed boosts and launch pads,
WalkSpeedchanges, ragdolls and explosions. Each one should reset the last position or raise the limit. - Check the limit has headroom. Roblox's network ownership guide notes position updates are subject to latency and need averaging over time, so a lagging player can appear to cover several seconds of distance in one sample.
- Roblox's server-side detection guide recommends rubber-banding a speeding player back rather than kicking them, and acting only once several different signals agree.
- Client-side movement checks are Anti-cheat that only runs on the client. Punishment triggered by other players' reports is Players punished on another client's word.
What an exploiter could do
Exploiters lose little from this; the cost falls on honest players. A lag spike, a laggy connection, a fling from another player's physics or one of the game's own pads gets innocent players kicked or banned. Players who can push or fling others can get them removed on purpose.
How to fix it
- Respond to a failed reading by moving the player back to their last good position, not by kicking.
- Keep a count of failures over a short window and act, if at all, only when it stays high, and log it for staff to review before any ban.
- Reset the last good position whenever the game moves the player itself, and allow for every speed source the game has.
Vulnerable example
Vulnerable
local Players = game:GetService("Players") local RunService = game:GetService("RunService") local allowance = 1.5 local lastPosition: { [Player]: Vector3 } = {} RunService.Heartbeat:Connect(function(delta) for _, player in Players:GetPlayers() do local character = player.Character local root = character and character:FindFirstChild("HumanoidRootPart") local humanoid = character and character:FindFirstChildOfClass("Humanoid") if root and root:IsA("BasePart") and humanoid then local position = root.Position * Vector3.new(1, 0, 1) local previous = lastPosition[player] lastPosition[player] = position if previous and (position - previous).Magnitude > humanoid.WalkSpeed * allowance * delta then player:Kick("Speed exploit detected.") end end end end) Players.PlayerRemoving:Connect(function(player) lastPosition[player] = nil end)
Fixed example
Fixed
local Players = game:GetService("Players") local RunService = game:GetService("RunService") local allowance = 1.5 local interval = 0.5 local lastPosition: { [Player]: Vector3 } = {} local elapsed = 0 RunService.Heartbeat:Connect(function(delta) elapsed += delta if elapsed < interval then return end for _, player in Players:GetPlayers() do local character = player.Character local root = character and character:FindFirstChild("HumanoidRootPart") local humanoid = character and character:FindFirstChildOfClass("Humanoid") if root and root:IsA("BasePart") and humanoid then local position = root.Position * Vector3.new(1, 0, 1) local previous = lastPosition[player] if previous and (position - previous).Magnitude > humanoid.WalkSpeed * allowance * elapsed then root.CFrame = CFrame.new(previous.X, root.Position.Y, previous.Z) else lastPosition[player] = position end end end elapsed = 0 end) Players.PlayerAdded:Connect(function(player) player.CharacterAdded:Connect(function() lastPosition[player] = nil end) end) Players.PlayerRemoving:Connect(function(player) lastPosition[player] = nil end)
Common false positives
- Checks that only move the player back, freeze them briefly or flag them for staff review don't punish innocent players for one bad reading.
- A kick after many failures across a long window, with the game's own movement allowed for, is a judgement call. Mention the risk rather than reporting it.
- Kicks for things no honest player can do at all, such as being in a sealed area that the server can prove they never entered, aren't measurement noise. Judge them on their own evidence.