How to send gifts between servers without losing them
MessagingService doesn't promise delivery, so gifts and staff changes sent only as messages can vanish. How to save them first and use messages to speed things up.
What it looks like
Something that must arrive is sent to other servers with MessagingService and nothing else: coins or items gifted to a friend in another server, a trade's payout, a staff member's removal, an unban or a refund. The sending server takes the cost or makes the change, publishes a message, and trusts that the receiving server will apply it.
profile.Coins -= amount
MessagingService:PublishAsync("Gifts", { to = friendId, coins = amount })How to detect it
- Find every
MessagingService:PublishAsynccall and what itsSubscribeAsynccallback does. Flag callbacks that give currency or items, change saved data, or grant or remove powers. - Roblox's
MessagingServicereference says delivery is best effort and not guaranteed, and to design games so a failed delivery isn't critical. A successfulPublishAsynconly means the message was sent. - Messages only reach servers that are running and subscribed at the time. Check what happens when the recipient is offline, between servers, or in a server that's starting up.
- Check whether the sender pays before the message is published. A refund when
PublishAsyncerrors doesn't cover a message that was sent but never arrived. - The safe pattern is to write the change to a data store first, such as a gift in the recipient's inbox or a removal in a saved list, and use the message only to tell running servers to apply it sooner. Servers then read the saved copy when a player joins or a server starts.
- Bans kept only in messages are reported under Bans that don't last.
What an exploiter could do
Nobody has to do anything. Gifts sent to an offline friend, or lost in delivery, disappear after the sender has paid for them. Staff removals and other changes reach only the servers that were listening, so a removed moderator keeps their powers in any server that missed the message and in every server that starts later.
How to fix it
- Save the change to a data store keyed by the recipient, and apply it when they join, as a mailbox.
- Keep the message as a fast path that tells running servers to load the saved change.
- Take the sender's payment and add the saved gift together, and give the payment back if the save fails.
Vulnerable example
local MessagingService = game:GetService("MessagingService") local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local coins: { [Player]: number } = {} ReplicatedStorage.Remotes.GiftFriend.OnServerEvent:Connect(function(player: Player, friendId: unknown, amount: unknown) if typeof(friendId) ~= "number" or typeof(amount) ~= "number" or amount ~= math.floor(amount) or amount < 1 then return end if (coins[player] or 0) < amount then return end coins[player] -= amount pcall(MessagingService.PublishAsync, MessagingService, "Gifts", { to = friendId, coins = amount }) end) MessagingService:SubscribeAsync("Gifts", function(message) local data = message.Data local recipient = Players:GetPlayerByUserId(data.to) if recipient then coins[recipient] = (coins[recipient] or 0) + data.coins end end) Players.PlayerRemoving:Connect(function(player) coins[player] = nil end)
Fixed example
local DataStoreService = game:GetService("DataStoreService") local MessagingService = game:GetService("MessagingService") local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local inboxes = DataStoreService:GetDataStore("GiftInboxes") local coins: { [Player]: number } = {} local function collect(player: Player) local gifted = 0 local ok = pcall(inboxes.UpdateAsync, inboxes, `Gifts_{player.UserId}`, function(saved: any): number? gifted = if typeof(saved) == "number" then saved else 0 return 0 end) if ok and gifted > 0 then coins[player] = (coins[player] or 0) + gifted end end ReplicatedStorage.Remotes.GiftFriend.OnServerEvent:Connect(function(player: Player, friendId: unknown, amount: unknown) if typeof(friendId) ~= "number" or friendId ~= math.floor(friendId) or friendId < 1 or friendId == player.UserId then return end if typeof(amount) ~= "number" or amount ~= math.floor(amount) or amount < 1 or amount > 1000 then return end if (coins[player] or 0) < amount then return end coins[player] -= amount local saved = pcall(inboxes.UpdateAsync, inboxes, `Gifts_{friendId}`, function(current: any): number return (if typeof(current) == "number" then current else 0) + amount end) if not saved then coins[player] += amount return end pcall(MessagingService.PublishAsync, MessagingService, "GiftSent", friendId) end) pcall(MessagingService.SubscribeAsync, MessagingService, "GiftSent", function(message) local recipient = if typeof(message.Data) == "number" then Players:GetPlayerByUserId(message.Data) else nil if recipient then collect(recipient) end end) Players.PlayerAdded:Connect(collect) Players.PlayerRemoving:Connect(function(player) coins[player] = nil end)
Common false positives
- Messages that only show something, such as announcements, rare-find shout-outs or server lists, can be lost without harm. Judge their volume with Players can flood cross-server messages.
- A message that tells servers to reload something already saved in a data store is the fix, not the finding.
- Changes that only matter in running servers and are meant to end with them, such as a live event starting, are fine to send by message alone.