How to stop one remote call freezing your server
Loop counts, lists and lengths sent by the client decide how much work the server does. How to cap sizes before the work starts.
What it looks like
The cost of one call depends on a number, string or table the client sends: a loop count, a list of positions, a search radius, a length. The handler may have a cooldown, but a single call can still ask for millions of parts or loop iterations.
Luau
remote.OnServerEvent:Connect(function(player, positions) for _, position in positions do makeBlock(position) end end)
How to detect it
- Trace each client number, string and table into loops,
Instance.new,Clone,table.create,string.rep, pattern functions, spatial queries likeGetPartBoundsInRadius, raycasts and pathfinding. - Check for an upper bound before the work starts. A type check, a whole-number check or a lower bound alone doesn't limit the size.
- For tables, the count has to be checked on the server.
#only counts the array part and can be misleading for tables with gaps, so count entries in a loop that stops as soon as it passes the limit. - Strings need a length cap before they're searched, split, repeated or stored.
- Nested loops over client data multiply: 1,000 entries checked against 1,000 entries is a million steps.
What an exploiter could do
One call can freeze the server for everyone: the server is busy with their request, so every other player's game stops updating until it finishes. A call that creates instances can leave the server slow until they're removed. A cooldown doesn't help, because the damage is done in a single call.
How to fix it
- Give every client-controlled size an upper bound and reject the call if it's over: list lengths, counts, lengths of strings, radii and distances.
- Count tables with a loop that bails out at the limit, then validate each entry.
- Keep heavy work proportional to something the server controls, like the player's plot size, rather than what the client asks for.
Vulnerable example
Vulnerable
local ReplicatedStorage = game:GetService("ReplicatedStorage") ReplicatedStorage.Remotes.PlaceBlocks.OnServerEvent:Connect(function(player: Player, positions: unknown) if typeof(positions) ~= "table" then return end for _, position in positions :: { unknown } do if typeof(position) == "Vector3" then local block = Instance.new("Part") block.Anchored = true block.Size = Vector3.new(4, 4, 4) block.Position = position block.Parent = workspace end end end)
Fixed example
Fixed
local ReplicatedStorage = game:GetService("ReplicatedStorage") local maxBlocks = 20 local function readPositions(value: unknown): { Vector3 }? if typeof(value) ~= "table" then return nil end local positions = {} for _, position in value :: { unknown } do if typeof(position) ~= "Vector3" or #positions >= maxBlocks then return nil end table.insert(positions, position) end return positions end ReplicatedStorage.Remotes.PlaceBlocks.OnServerEvent:Connect(function(player: Player, value: unknown) local positions = readPositions(value) if not positions then return end for _, position in positions do local block = Instance.new("Part") block.Anchored = true block.Size = Vector3.new(4, 4, 4) block.Position = position block.Parent = workspace end end)
Common false positives
- Sizes that are bounded somewhere the handler calls, like a helper that validates the list, count. Read it before reporting.
- Small fixed loops, such as checking a handful of allowlisted slots, don't depend on the client.
- Where the bound is there but generous, mention it as a tuning point rather than a finding.