How often players should be able to change a sign
Roblox requires at least a minute between changes to text other players can see. How to rate limit signs and boards on the server.
What it looks like
Players can write text that others see: a sign on their plot, a note on a bulletin board, a shop banner or a message on a whiteboard. The server filters it and has a cooldown, but the cooldown is shorter than a minute, or there isn't one.
Luau
local cooldown = 5 remotes.SetSign.OnServerEvent:Connect(function(player, text) if os.clock() - (lastEdit[player] or 0) < cooldown then return end ... end)
How to detect it
- Find every place a player's text is shown to other players outside chat: signs, boards, notes, banners, plot or shop names, and any
SurfaceGuiorBillboardGuitext the server sets from a player's input. - Roblox's chat guidelines say games with editable text that users can see between each other, such as signs or bulletin boards, must add rate limits of at least one minute. Its rate-limited text input example uses one post per player per minute.
- Read the cooldown value and how it's applied. It must be per player, measured on the server, and at least 60 seconds between changes.
- Check that the time is recorded only when the text is actually shown, and that a failed filter doesn't reset it in a way that lets text through faster.
- Roblox's filtering guide says not to filter text per character as it's typed. A remote fired from a
TextBoxtext-change event usually means the text is shown, and the limit tested, on every keystroke.
What an exploiter could do
They turn a sign or board into a chat window outside TextChatService, holding a conversation with other players that skips their chat settings and can't be reported. Changing it every few seconds also lets them flash filtered-but-unwanted messages at everyone nearby.
How to fix it
- Set the per-player cooldown to at least 60 seconds for any text other players can see.
- Record the time on the server when the text is shown, and refuse earlier changes.
- For real conversations, use a
TextChannelinstead. See Player chat sent through remotes instead of TextChatService.
Vulnerable example
Vulnerable
local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local TextService = game:GetService("TextService") local board = workspace:WaitForChild("NoticeBoard"):WaitForChild("SurfaceGui"):WaitForChild("Note") :: TextLabel local cooldown = 3 local lastPost: { [Player]: number } = {} ReplicatedStorage.Remotes.PostNote.OnServerEvent:Connect(function(player: Player, text: unknown) if typeof(text) ~= "string" or #text > 100 then return end if os.clock() - (lastPost[player] or 0) < cooldown then return end local ok, filtered = pcall(function() return TextService:FilterStringAsync(text, player.UserId):GetNonChatStringForBroadcastAsync() end) if ok then lastPost[player] = os.clock() board.Text = filtered end end) Players.PlayerRemoving:Connect(function(player) lastPost[player] = nil end)
Fixed example
Fixed
local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local TextService = game:GetService("TextService") local board = workspace:WaitForChild("NoticeBoard"):WaitForChild("SurfaceGui"):WaitForChild("Note") :: TextLabel local cooldown = 60 local lastPost: { [Player]: number } = {} ReplicatedStorage.Remotes.PostNote.OnServerEvent:Connect(function(player: Player, text: unknown) if typeof(text) ~= "string" or #text > 100 then return end if os.clock() - (lastPost[player] or 0) < cooldown then return end local ok, filtered = pcall(function() return TextService:FilterStringAsync(text, player.UserId):GetNonChatStringForBroadcastAsync() end) if ok then lastPost[player] = os.clock() board.Text = filtered end end) Players.PlayerRemoving:Connect(function(player) lastPost[player] = nil end)
Common false positives
- Names for a player's own things, such as a pet's name, are listed in Roblox's chat guidelines as text that isn't chat. They still need filtering, but a short cooldown isn't this finding.
- Text only the writer can see, such as a private note, isn't shared between users.
- Text the game writes itself, such as a round timer or a top-player sign, isn't player text.
- Missing filtering is Player text shown to others without filtering, reported separately.