How to ban players properly in Roblox
Bans kept in a script table or saved by username don't stop anyone for long. How to use Players:BanAsync or save bans by UserId.
What it looks like
The game bans players, but the ban only lives in one server's memory, is saved under the player's name, or is just a kick. The banned player joins another server, rejoins later or changes their name, and plays as normal.
Luau
banned[target.UserId] = true
target:Kick("Banned")How to detect it
- Find every ban: admin commands, moderation remotes, anti-cheat that removes players, and any table or data store named like
bansorblacklist. - Check where the ban is kept. A table in a script is gone when that server closes and was never seen by other servers. A ban that's only a
Kickisn't a ban. - Check what happens when the lookup fails. A ban check in
pcallthat lets the player in when the data store errors is part of the finding. - Check what the ban is keyed on. It should be the
UserId, which never changes.NameandDisplayNamecan be changed by the player. - Check that every join is checked:
PlayerAddedmust look the ban up, and players already in the server when the script starts must be checked too. - If the game uses
Players:BanAsync, check it's called inpcalland thatPlayers.BanningEnabledhasn't been turned off.
What an exploiter could do
A banned exploiter is back within a minute by joining another server, or after a name change. Moderators think the problem is dealt with while it carries on, and players stop trusting reports.
How to fix it
- Use
Players:BanAsync, Roblox's built-in ban. It's keyed byUserId, removes banned players who are in the server, stops them joining again, covers every place in the experience unlessApplyToUniverseis set to false, and by default covers suspected alt accounts too. - If you keep your own bans, save them in a data store keyed by
UserId, check them on every join, and retry a failed lookup. Decide what happens if it still fails: removing the player is safer, but a data store outage then removes everyone. - Record who banned whom and why, so bans can be reviewed and undone with
Players:UnbanAsync.
Vulnerable example
Vulnerable
local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local moderators: { [number]: boolean } = { [123456789] = true, } local banned: { [number]: boolean } = {} ReplicatedStorage.Remotes.ModBan.OnServerEvent:Connect(function(player: Player, target: unknown) if not moderators[player.UserId] then return end if typeof(target) ~= "Instance" or not target:IsA("Player") or target == player then return end banned[target.UserId] = true target:Kick("You are banned.") end) Players.PlayerAdded:Connect(function(player) if banned[player.UserId] then player:Kick("You are banned.") end end)
Fixed example
Fixed
local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local moderators: { [number]: boolean } = { [123456789] = true, } ReplicatedStorage.Remotes.ModBan.OnServerEvent:Connect(function(player: Player, target: unknown) if not moderators[player.UserId] then return end if typeof(target) ~= "Instance" or not target:IsA("Player") or target == player then return end local ok, err = pcall(Players.BanAsync, Players, { UserIds = { target.UserId }, Duration = -1, DisplayReason = "You are banned for exploiting.", PrivateReason = `Banned by {player.UserId}`, ExcludeAltAccounts = false, ApplyToUniverse = true, }) if not ok then warn(`Ban failed for {target.UserId}: {err}`) end end)
Common false positives
- A kick on its own is fine when it isn't meant as a ban, such as removing someone from a round or a server-side speed check that gives up.
- Short bans kept on purpose for one server, like a vote-kick that only lasts until the server ends, are fine if the game says so.
- Bans handled by a well-known admin module that saves them by
UserIdaren't this finding. Check the module's settings rather than its code.