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.

warningLuaudit check: client-sized-work

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 like GetPartBoundsInRadius, 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.