How to stop connections piling up on your server
Connecting to Heartbeat or starting a loop on every respawn or remote call adds work that never stops. How to keep one connection per player or one shared loop.
What it looks like
Each remote call, respawn or equip connects a new event handler or starts a new loop, and nothing stops the old one. Every repeat adds more work that runs for the rest of the server's life.
Luau
remote.OnServerEvent:Connect(function(player) RunService.Heartbeat:Connect(function() spin(player) end) end)
How to detect it
- Look for
:Connect,task.spawn,task.delay,coroutine.wrapandwhileloops inside remote handlers,CharacterAdded,Equipped,Touchedand other events that fire again and again. - For each one, find what stops it: a stored connection that's disconnected, a loop condition that ends, or the instance it's connected to being destroyed.
- Destroying an instance disconnects connections to its own events. Connections to
RunService,Players, aPlayer,workspaceor any other long-lived object stay until they're disconnected. - Loops that check a flag should check one that belongs to this run. A shared "enabled" flag switched off and on again leaves the first loop running beside the second.
What an exploiter could do
They repeat whatever starts the work, by firing the remote, respawning or re-equipping a tool, and each repeat adds another handler or loop. The server spends more and more time on them every frame and holds on to more memory, until it's slow for everyone in it. Unlike a burst of calls, the cost stays after they stop.
How to fix it
- Keep each player's connection or loop in a table. Disconnect or stop the old one before starting a new one, and clean up in
Players.PlayerRemoving. - Prefer one shared loop that goes over a table of active players, over a loop per player or per call.
- Connect to the character's own parts and humanoid where you can, and make sure old characters are destroyed: turn on
Workspace.PlayerCharacterDestroyBehavior, or destroy old characters yourself once they have been replaced.
Vulnerable example
Vulnerable
local ReplicatedStorage = game:GetService("ReplicatedStorage") local RunService = game:GetService("RunService") ReplicatedStorage.Remotes.ToggleAura.OnServerEvent:Connect(function(player: Player, enabled: unknown) if typeof(enabled) ~= "boolean" then return end player:SetAttribute("Aura", enabled) if not enabled then return end RunService.Heartbeat:Connect(function() local character = player.Character local aura = character and character:FindFirstChild("Aura") if aura and aura:IsA("BasePart") and player:GetAttribute("Aura") then aura.CFrame *= CFrame.Angles(0, math.rad(2), 0) end end) end)
Fixed example
Fixed
local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local RunService = game:GetService("RunService") local active: { [Player]: boolean } = {} ReplicatedStorage.Remotes.ToggleAura.OnServerEvent:Connect(function(player: Player, enabled: unknown) if typeof(enabled) ~= "boolean" then return end if enabled then active[player] = true else active[player] = nil end end) Players.PlayerRemoving:Connect(function(player) active[player] = nil end) RunService.Heartbeat:Connect(function() for player in active do local character = player.Character local aura = character and character:FindFirstChild("Aura") if aura and aura:IsA("BasePart") then aura.CFrame *= CFrame.Angles(0, math.rad(2), 0) end end end)
Common false positives
- Connections made once at startup, or once per player in
PlayerAdded, don't pile up. - Connections to parts inside the character, or to its
Humanoid, end when the character is destroyed. Roblox destroys old characters on respawn whenWorkspace.PlayerCharacterDestroyBehavioris enabled. Otherwise check the game destroys them itself. - A loop that checks something unique to its own run, like whether its character is still the player's current one, ends by itself.