Skip to content

Filter player-written text

The moment your game lets one player type something another player will read, you have taken on a moderation obligation that Roblox enforces. Most of the work is one function call. The part that goes wrong is what happens when that call fails, because the tempting fallback is the one that gets games taken down.

Any text one player wrote and another player sees has to go through Roblox’s filter. This is not a style preference. A game that shows it unfiltered can be taken down.

It applies to every surface you built yourself: pet names, guild names, signs, announcements, chat replacements, anything typed into a text box. The default chat is already filtered. Nothing you wrote is.

Twill.Net.Handle(Remotes.RenamePet, function(player, petId, name)
-- Filtering yields, so everything after it has to assume time has passed.
local shown = Twill.Filter.ForBroadcast(name, player.UserId)
if not shown then
return false, "could not check that name, try again"
end
-- Re-read after the yield rather than before it. The player may have left
-- while the filter was thinking, and their session is gone with them.
local data = Twill.Data.Get(player)
if not data then
return false, "not ready"
end
-- Store the filtered text, never the text they submitted.
data.Pets[petId].Name = shown
return true, shown
end, {
Rate = 0.5,
Schema = { { "string", 1, 40 }, { "string", 1, 20 } },
Reject = function() return false, "slow down" end,
})

Store the filtered result and display the stored result. Do not filter again on every draw.

Every function in Filter answers nil when the filter cannot be reached. It never returns the text that went in.

Do this
local shown = Twill.Filter.ForBroadcast(text, userId)
if not shown then
return refuse("try again in a moment")
end
Not this
local shown = Twill.Filter.ForBroadcast(text, userId) or text

The or text fallback looks like defensive programming and is the opposite of it. A filter outage becomes an unfiltered broadcast.

FunctionResultCost
ForBroadcastOne string safe for everybody.One call.
ForUserTailored to a single recipient.One call per recipient.

ForBroadcast is what almost every use wants. Reach for ForUser only where the recipient genuinely matters, such as a direct message, and never in a loop over the whole server.

For a name, showing ##### is worse than refusing.

if not Twill.Filter.IsClean(name, player.UserId) then
return false, "pick a different name"
end

IsClean returns false when the filter could not be reached, consistent with the rest of the module. That means a filter outage refuses the rename, which is the correct direction to fail.

The platform limits filtering per user, and the underlying calls spend the server’s web quota. Filter adds no limit of its own.

Filter once at submission. Store the result. Show the stored result.