How to stop busy flags locking a feature for good
A handler that sets a busy flag and then returns early or errors leaves the feature locked. How to check first and clear the flag on every path.
What it looks like
A handler sets a flag so only one thing happens at a time: a machine in use, a pending trade request for a player, a round starting, a purchase being processed. Some path through the handler then stops before the flag is cleared: an early return after the flag is set, a call that can error, or a yield that never resumes. The flag stays set and the feature stays locked.
inUse = true if typeof(stake) ~= "number" then return end
What an exploiter could do
They send one call that stops halfway, for example with an argument the handler rejects after setting the flag. A shared machine, event or minigame stays "in use" for the rest of the server, or another player is left with a request that never clears, so nobody can trade with them or challenge them until they rejoin.
How to fix it
- Check every argument and condition before setting the flag.
- Wrap the work between setting and clearing the flag in
pcall, and clear the flag whatever the result. - Store when the flag was set and treat it as clear after a timeout, so a stuck flag can't last.
- Clear flags in
PlayerRemoving, and in failure events such asTeleportInitFailed.
Vulnerable example
local MarketplaceService = game:GetService("MarketplaceService") local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local gamePassId = 123456789 local requested: { [Player]: Player } = {} ReplicatedStorage.Remotes.RequestCarry.OnServerEvent:Connect(function(player: Player, target: unknown) if typeof(target) ~= "Instance" or not target:IsA("Player") or target == player or requested[target] then return end requested[target] = player local owns = MarketplaceService:UserOwnsGamePassAsync(player.UserId, gamePassId) if owns then ReplicatedStorage.Remotes.CarryRequested:FireClient(target, player) else requested[target] = nil end end) Players.PlayerRemoving:Connect(function(player) requested[player] = nil end)
Fixed example
local MarketplaceService = game:GetService("MarketplaceService") local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local gamePassId = 123456789 local requestTimeout = 30 local requested: { [Player]: { from: Player, at: number } } = {} ReplicatedStorage.Remotes.RequestCarry.OnServerEvent:Connect(function(player: Player, target: unknown) if typeof(target) ~= "Instance" or not target:IsA("Player") or target == player then return end local current = requested[target] if current and os.clock() - current.at < requestTimeout then return end local ok, owns = pcall(MarketplaceService.UserOwnsGamePassAsync, MarketplaceService, player.UserId, gamePassId) if not ok or not owns or target.Parent ~= Players then return end requested[target] = { from = player, at = os.clock() } ReplicatedStorage.Remotes.CarryRequested:FireClient(target, player) end) Players.PlayerRemoving:Connect(function(player) requested[player] = nil end)
How to spot this in your own scripts, and the cases that only look like it, are part of the Luaudit check your AI runs.