Text filtering: the game goes offline until you add it, and the responsibility is yours
Roblox filters chat by itself. Every other piece of text the game displays is the responsibility of whoever built it — and a game detected without filtering is removed until the filter exists.
Produced with AI from the sources cited below, and checked by automated verification against those same sources. No person read this text before publication. Editorial Policy

There is a sentence in the text filtering documentation that changes the weight of the whole subject, and it is not written as a warning: if Roblox receives reports or automatically detects that your game does not apply text filtering, the system removes the game until you add the filter.
It is not a warning, it is not reduced reach. It is the game taken offline. This guide covers what has to be filtered, how filtering works, and the mistakes that leave holes.
The problem
What the filter does: it stops players seeing inappropriate language and personal information, such as a phone number. Both matter, and the second is usually forgotten — the filter does not exist only against swearing.
The division of responsibility is where almost everybody goes wrong, and it fits in two sentences. Roblox automatically filters the common outputs, such as a message that went through the in-game chat. And the responsibility for filtering any displayed text you do not explicitly control is yours.
The misunderstanding is thinking "Roblox filters it" applies to everything. It applies to chat. The rest — a sign, a pet name, a nickname, a board, anywhere a player writes and another person reads — belongs to the game.
And the documentation lists four scenarios, which together cover practically every case:
- Direct player input: through a
TextBox, through a keyboard interface built from buttons, or through an interactive keyboard in 3D space. - Generated text: a game that generates words from random characters and displays them. It is the scenario nobody anticipates — there is a chance of creating an inappropriate word by accident, and at scale the chance is not small.
- External content: a game that fetches content from an external server to display in-game. The documentation explains why that is dangerous even when it is your own server: it is common not to control the content, and a third party can edit the information.
- Stored text: text in a
DataStore— a pet name is the example given — that may contain an inappropriate word and has to be filtered on read.
That last one deserves attention because it is retroactive. Every piece of text your players saved before you implemented the filter is still there, and it will appear on screen the next time somebody loads that save.
Step by step
1. Collect on the client, filter on the server. The official example collects the input on the FocusLost event and sends it to the server through a RemoteEvent. On the server, the text goes first through FilterStringAsync and then through the delivery method.
2. Call FilterStringAsync. It takes the text and the UserId of whoever wrote it. The UserId is not a formality: it is what tells the filter whose text this is.
3. Choose the right method on the result. The call returns a TextFilterResult object, with two additional methods, and the choice between them is about who is going to read:
- One is for text visible to all players on the server — the case of a sign everyone can see.
- The other displays the filtered text to a specific player, taking their age and other details into account.
That difference is real and not cosmetic. The per-user filter is more or less restrictive depending on who is reading; the broadcast one has to serve everyone at once, and so it tightens further.
4. Filter after submission, not during. The documentation is explicit: do not filter in real time, on every character typed into the TextBox. The technical reason is that this produces text visible only to whoever is typing. The guidance is to filter after the player submits.
5. Treat failure as an absence of text. This step is not in the documentation as a step, but it is what decides whether the implementation survives a bad network day. The next section goes into it.
It is also worth understanding why the two methods exist instead of one. The filter is not a fixed list of forbidden words: it is sensitive to who reads. What an adult sees and what a child sees are not the same thing, and the platform knows the difference. When the text goes to one person, their criterion can be applied; when it goes to the whole server, the filter has to serve the most restricted reader who might be there.
From that comes a design consequence: public text is filtered more than private text. If your sign comes out over-censored, the answer is not switching method — it is accepting that text everyone reads has a tighter standard, and that this is the requested behaviour.
The artifact
The module below exposes both methods with names that state the decision — forEveryone and forOne — and brings the three uses in the footer.
The rule worth copying even if the rest is no use is in the comment in the middle: on failure, do not show the original.
The filter is a network call, and network calls fail. The temptation, when handling the error, is to fall back to the text the player wrote — after all, it is better to show something. That line turns a momentary instability into unfiltered text on screen, which is exactly the condition that takes the game offline. Every function in the artifact returns nil on failure, and the caller treats nil as "I have no text to show".
In the sign's case, that means keeping the previous text. Old text is better than raw text.
Common mistakes
Filtering on the client. The most frequent and the most useless mistake. The filter runs on the server because the client is not trustworthy — a modified client simply does not call the function, and the text reaches the server raw. Filtering on the client gives a sense of protection and protects nothing.
Filtering on every keystroke. Against the explicit guidance, and on top of that it costs a network call per character. FocusLost exists for this.
Forgetting stored text. It is the most common hole in a game that already exists. You implement the filter on input, test it, it works — and all the text saved before keeps coming out unfiltered on read. The fix is filtering on read too, which looks redundant and is not: the old data never passed through any filter.
Trusting your own external server. If the game fetches text from an API of yours, it is natural to think that content is controlled. The documentation dismantles that: a third party can edit the information. And even without a third party, a mistake of yours in the API becomes unfiltered text in the game — with no filter at the boundary, your API becomes part of the game's moderation surface.
Passing the wrong UserId. The UserId belongs to whoever wrote it, not whoever is reading. In the case of a pet name read from a DataStore, the writer was the save's owner, possibly months ago, and they may not even be on the server. Passing the reader's UserId changes what the filter considers.
Not validating before filtering. The filter is not validation: it does not check length, it does not check type, it does not limit frequency. A string of a million characters reaches FilterStringAsync and becomes an expensive network call. Validating type, length and rate comes first.
One further care applies to a game already live: implementing the filter is not the end of the work, because the surface grows by itself. Every new feature that accepts text — a nickname field, a message box, a clan system with names — is born unfiltered unless somebody remembers. The defence that survives is having a single door through which all player text passes, like the artifact's module, instead of repeating the call in each place.
Next step
Today's concrete exercise is a sweep, and it is short: list every place in your game where text that came from outside appears on screen. That means typed text, generated text, text read from a database and text fetched from the web.
For each item on the list, answer two things: does it pass through the filter? And does the filter run on the server?
The list is usually longer than memory suggests. The obvious TextBox is there, but so is the team name somebody chose, the nickname on the leaderboard, the board text, the description of the item the player named. Each of them is a place where the game shows one person something another wrote — which is the definition of what has to be filtered.
And, if the game has text saved from before, it is worth scheduling filtered reads as a priority. There is no way to know what is stored until somebody loads it — and when they load it, it is already on screen.
Sources
- Text filtering | Documentation - Roblox Creator Hub — checked on
Ready-to-use artifact
-- ServerScriptService: the only place in the game that filters text.
--
-- The choice between the two methods is NOT taste -- it is about WHO IS
-- GOING TO READ, and getting it wrong means either showing what should
-- not be shown, or censoring what did not need it:
--
-- forEveryone -> the text appears for the whole server. A sign, a pet
-- name, a message on a board.
-- forOne -> the text appears for ONE person, and the filter takes
-- THEIR age and other details into account.
local TextService = game:GetService("TextService")
local Filter = {}
-- Common step: FilterStringAsync with the text and the UserId of WHOEVER
-- WROTE IT. The UserId is not decoration: it tells the filter whose text
-- this is.
local function result(text, fromUserId)
local r
local ok, err = pcall(function()
r = TextService:FilterStringAsync(text, fromUserId)
end)
if not ok then
warn("FilterStringAsync failed: " .. tostring(err))
return nil
end
return r
end
--[[
THE RULE THAT SAVES THE GAME: on failure, DO NOT SHOW THE ORIGINAL.
The filter is a network call and it can fail. The temptation is "if it
failed, show what the player wrote" -- and that line turns a network
failure into unfiltered text on screen, which is exactly what gets the
game taken down. Every function below returns nil on failure, and the
caller has to treat nil as "I have no text to show".
]]
function Filter.forEveryone(text, fromUserId)
if type(text) ~= "string" or text == "" then
return nil
end
local r = result(text, fromUserId)
if not r then
return nil
end
local ok, filtered = pcall(function()
return r:GetNonChatStringForBroadcastAsync()
end)
return ok and filtered or nil
end
function Filter.forOne(text, fromUserId, toUserId)
if type(text) ~= "string" or text == "" then
return nil
end
local r = result(text, fromUserId)
if not r then
return nil
end
local ok, filtered = pcall(function()
return r:GetNonChatStringForUserAsync(toUserId)
end)
return ok and filtered or nil
end
--[[
═══════════════════════════════════════════════════════════════════════
THE THREE USES
═══════════════════════════════════════════════════════════════════════
-- 1. TEXT INPUT. The client sends on FocusLost; the server filters.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local remote = ReplicatedStorage:WaitForChild("SignText")
local sign = workspace:WaitForChild("Sign").SurfaceGui.TextLabel
remote.OnServerEvent:Connect(function(player, text)
-- validate BEFORE filtering: type, length, rate
if type(text) ~= "string" or #text == 0 or #text > 200 then return end
local clean = Filter.forEveryone(text, player.UserId)
if clean then
sign.Text = clean
end
-- if clean is nil, the sign does NOT change. Old text beats raw text.
end)
-- 2. TEXT FROM A DATASTORE. Saved before the rule, or saved by an old
-- version of the game: filter on READ, do not trust what is there.
local function petName(player, savedName, ownerUserId)
return Filter.forOne(savedName, ownerUserId, player.UserId)
end
-- 3. CONTENT FROM AN EXTERNAL SERVER. You do not control what arrives,
-- and a third party can edit the source at any moment. Filter it the
-- same way. The UserId here is the RECEIVER's, absent a known author.
local function externalText(player, fromTheWeb)
return Filter.forOne(fromTheWeb, player.UserId, player.UserId)
end
]]
return Filter


