How to rate limit MessagingService publishes from players
One player firing a remote that publishes to a shared topic can use up its budget for every server. How to add per-player limits and validate what you receive.
What it looks like
A remote, chat command or other player action calls MessagingService:PublishAsync with no per-player limit, often on a topic every server listens to: global announcements, cross-server trade boards, friend finders or "player X just hatched a legendary" shout-outs.
remotes.Shout.OnServerEvent:Connect(function(player, text) MessagingService:PublishAsync("Shouts", filtered(player, text)) end)
How to detect it
- Find every
MessagingService:PublishAsynccall and trace back what triggers it. Calls a player can cause, directly or through a remote, chat, a purchase prompt or a touch, need a server-side per-player limit. - Roblox's limits, per the
MessagingServicereference: each server can send600 + 240 × playersmessages a minute, and each topic can receive40 + 80 × serversmessages a minute across the whole game. One server sending fast enough can use up a shared topic's receive budget for every server. - Check the limit is a per-player cooldown or budget kept on the server. A cooldown on the client, or one shared by the whole server, isn't enough; see One cooldown shared by every player and Cooldown the client can reset.
PublishAsyncis a web call and can error. Check it's inpcalland that a failure doesn't leave the player's own action half done.- Look at the receiving side too.
SubscribeAsynccallbacks get data that came from a player in another server. Treat it like remote arguments: check types and sizes, and filter text for this server's players. Unfiltered text is Player text shown to others without filtering.
What an exploiter could do
One player fires the remote as fast as they can and fills the topic. Every server's shout feed, trade board or event messages slow down or stop arriving for everyone, and other systems sharing the topic or the game's limits, such as cross-server moderation or live events, are pushed over them too. Messages carry up to 1 kB each, so a flood also costs every receiving server work.
How to fix it
- Give each player a server-side cooldown or token bucket for every action that publishes, such as one shout a minute.
- Batch messages that don't need to arrive at once, such as shout-outs, and publish them from one server loop.
- Keep separate topics for separate systems so one flood doesn't use up another topic's budget, and validate everything a subscription receives. All topics still share the game-wide receive limit of 400 + 200 × servers messages a minute, so per-player limits are still needed.
Vulnerable example
local MessagingService = game:GetService("MessagingService") local ReplicatedStorage = game:GetService("ReplicatedStorage") ReplicatedStorage.Remotes.ShareFind.OnServerEvent:Connect(function(player: Player, item: unknown) if typeof(item) ~= "string" or #item > 30 then return end pcall(MessagingService.PublishAsync, MessagingService, "GlobalFinds", { userId = player.UserId, item = item, }) end)
Fixed example
local MessagingService = game:GetService("MessagingService") local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local PlayerData = require(script.Parent.PlayerData) local cooldown = 60 local lastShare: { [Player]: number } = {} ReplicatedStorage.Remotes.ShareFind.OnServerEvent:Connect(function(player: Player, item: unknown) local profile = PlayerData.get(player) if typeof(item) ~= "string" or not profile or not table.find(profile.Inventory, item) then return end local now = os.clock() if now - (lastShare[player] or -math.huge) < cooldown then return end lastShare[player] = now pcall(MessagingService.PublishAsync, MessagingService, "GlobalFinds", { userId = player.UserId, item = item, }) end) Players.PlayerRemoving:Connect(function(player) lastShare[player] = nil end)
Common false positives
- Publishes the server makes on its own schedule, such as a server list update every minute, aren't player-driven.
- Publishing from a developer product's
ProcessReceipt, such as a global "thanks for donating" message, is limited by how fast players can pay, though a cooldown is still sensible. - A per-player cooldown kept on the server, like the fixed example, is the fix.