Memory leak progressively breaks tag system

Hello!
I’ve been working on this tag system for an upcoming project. I’ve got the tag system to finally work, however there seems to be some kind of memory leak that causes more and more server lag for every tag. (See video below). My guess is that it has something to do with the runservice I create on the client to check a spatial hitbox. Not sure how to resolve it or debug it though.

I’ve tried checking memory usage on server and client but I can’t seem to find any outstanding problems. I would really appreciate if someone could give some advice on how to find/get rid of or properly disconnect the runservice perhaps? Thanks!

In the video below you can see how after a couple tags (25-30s in) it starts becoming unstable and the tags take longer and longer to go through until it eventually breaks.

Video link:

This is the local script:

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Events = ReplicatedStorage.Events
local UI_Events = ReplicatedStorage.UI_Events

local LocalPlayer = game.Players.LocalPlayer -- Corrected way to get the local player
local character = LocalPlayer.Character or LocalPlayer.CharacterAdded:Wait()
local isTagged = character:WaitForChild("isTagged")

-- Events
local InitiateTagHandlerEvent = Events:WaitForChild("InitiateTagHandlerEvent")
local TagPlayerEvent = Events:WaitForChild("TagPlayerEvent")
local RemoveDynamiteEvent = Events:WaitForChild("RemoveDynamiteEvent")
local DisplayTagUIRemote = UI_Events:WaitForChild("DisplayTagUIRemote")

-- Animation
local humanoid = character:WaitForChild("Humanoid")
local animator = humanoid:WaitForChild("Animator")

local HoldAnimation = Instance.new("Animation")
HoldAnimation.AnimationId = "rbxassetid://126486527881326"
local holdDynamiteAnimationTrack = animator:LoadAnimation(HoldAnimation)

local RunServiceConnection -- Declare for access between functions

-- Function to check the status of the enemy and tag them if they are not tagged
local function checkEnemyStatus(enemy_Character)
	local enemyIsTagged = enemy_Character:FindFirstChild("isTagged").Value
	local enemyPlayer = game.Players:GetPlayerFromCharacter(enemy_Character)
	wait()

	if enemyIsTagged == false then
		print("Enemy is not tagged")

		RunServiceConnection:Disconnect() -- Stop RunService when tagging happens
		TagPlayerEvent:FireServer(enemyPlayer) -- Notify server to tag the enemy player
	else
		print("Enemy is already tagged") -- Runs 60 times per second ish
	end
end

-- Function to initiate tagging ability
local function InitiateTagHandler()
	local ourHumanoidRootPart = character:WaitForChild("HumanoidRootPart")
	local debounce = false

	RunServiceConnection = RunService.Heartbeat:Connect(function()
		local overlapParams = OverlapParams.new() 
		local hitContents = workspace:GetPartBoundsInBox(
			ourHumanoidRootPart.CFrame * CFrame.new(0, 0, -3), Vector3.new(2.5, 3, 3), overlapParams)

		local hitList = {}
		for _, object in pairs(hitContents) do
			if object.Parent:FindFirstChild("Humanoid") and not table.find(hitList, object.Parent) then
				table.insert(hitList, object.Parent) -- Add the player's character to hit list
				local enemy_Character = object.Parent
				checkEnemyStatus(enemy_Character)
			end
		end
	end)
end

-- Event triggered when the player becomes the tagger
InitiateTagHandlerEvent.OnClientEvent:Connect(function()
	InitiateTagHandler()
end)

-- Event handler for when the player's tag status changes
isTagged.Changed:Connect(function()
	if isTagged.Value == true then
		InitiateTagHandler()
		holdDynamiteAnimationTrack:Play() -- Play holding animation
	else
		if RunServiceConnection then
			RunServiceConnection:Disconnect() -- Stop checking
		end
		holdDynamiteAnimationTrack:Stop()
	end
end)


local function DisplayTagUI (status, sendingPlr) -- Sending plr is the player giving the dynamite

	if status == "gotTagged" then -- The local player got tagged
		print("I got tagged")
		local NotificationsScreenGui = LocalPlayer.PlayerGui:FindFirstChild("Notifications")
		local NotificationFrame = NotificationsScreenGui.NotificationFrame
		local NotificationText = NotificationsScreenGui:FindFirstChild("NotificationFrame").TextLabel

		-- Tween the showing of label appearing & disappearing
		NotificationText.Text = sendingPlr.Name.." gave you the dynamite!"
		NotificationFrame.Visible = true
		wait(3)
		NotificationFrame.Visible = false

	elseif status == "tagged" then -- Local player tagged someone else

		local NotificationsScreenGui = LocalPlayer.PlayerGui:FindFirstChild("Notifications")
		local NotificationFrame = NotificationsScreenGui.NotificationFrame
		local NotificationText = NotificationsScreenGui:FindFirstChild("NotificationFrame").TextLabel

		NotificationText.Text = "You passed the dynamite to someone else!"
		NotificationFrame.Visible = true
		wait(3)
		NotificationFrame.Visible = false
	end

end

DisplayTagUIRemote.OnClientEvent:Connect(function(status, sendingPlr)
	DisplayTagUI(status, sendingPlr) -- Passes the sendingplr and receivingplr
end)

This is the server script:

local ServerStorage = game:GetService("ServerStorage")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")

local DynamiteModel = ServerStorage.Dynamites:WaitForChild("Default_Dynamite") -- Default dynamite model

-- Services
local CollectionService = game:GetService("CollectionService")
local RunService = game:GetService("RunService")

local Events = ReplicatedStorage.Events
local UI_Events = ReplicatedStorage.UI_Events

-- Events
local InitiateTagHandlerEvent = Events:WaitForChild("InitiateTagHandlerEvent")
local TagPlayerEvent = Events:WaitForChild("TagPlayerEvent")
local RemoveDynamiteEvent = Events:WaitForChild("RemoveDynamiteEvent")
local DisplayTagUIRemote = UI_Events:WaitForChild("DisplayTagUIRemote")

-- Modules
local SkinModule = require(ServerScriptService.Modules.SkinModule)
local DynamiteSkinModule = require(ServerScriptService.Modules.DynamiteSkinModule)


local dynamitePart = game.Workspace:WaitForChild("dynamitePart") -- For testing


local CooldownDuration = 1
local cooldownInfo = {} -- Stores information about the players and their tagged status

-- Remove dynamite when the player is no longer tagged
local function RemoveDynamite(player)
	local character = player.Character or player.CharacterAdded:Wait()
	local isTagged = character:FindFirstChild("isTagged")
	if isTagged.Value == false then
		-- Remove anything with a dynamite tag inside it
		for _, item in ipairs(character:GetChildren()) do
			if CollectionService:HasTag(item, "Dynamite") then
				item:Destroy()
			end
		end
		-- Remove highlight
		for _, item in ipairs(character:GetChildren()) do
			if item:IsA("Highlight") then
				item:Destroy()
			end
		end
	end
end


-- Create detonator (First person with the dynamite), give them dynamite + skin + attribute (true)
local function MakePlayerDetonator(player) -- Plr that should be made into detonator
	local character = player.Character or player.CharacterAdded:Wait()
	local SelectedSkin = "Professor" -- Change this later to their actual selected skin if it doesnt exist then go to default
	local SelectedDynamiteSkin = "Default_Dynamite" -- Change this later to their actual selected dynamite skin


	local isTagged = character:FindFirstChild("isTagged")
	isTagged.Value = true -- Make them "tagged"
	local DetonatorInfo = character:GetAttribute("DetonatorInfo")
	character:SetAttribute("DetonatorInfo", true) -- Make the player detonator in attribute

	InitiateTagHandlerEvent:FireClient(player)


	-- Double check that the player owns the skin before giving them it, otherwise give default

	SkinModule.ApplySkin(character, SelectedSkin) -- Apply the players selected detonator skin
	DynamiteSkinModule.ApplyDynamite(character, SelectedDynamiteSkin) -- Give player their selected dynamite skin


end


-- Temporary touched event to fire MakePlayerDetonator function

dynamitePart.Touched:Connect(function(hit)
	if hit.Parent:FindFirstChild("Humanoid") then
		local character = hit.Parent
		local player = game.Players:GetPlayerFromCharacter(character)

		MakePlayerDetonator(player) -- Make the player the detonator with skin etc
		wait(3)
	end
end)

local function AssignDynamite(player, enemyPlayer)
	print(player.Name, "is tagging", enemyPlayer.Name)
	local playerCharacter = player.Character or player.CharacterAdded:Wait()
	local enemyCharacter = enemyPlayer.Character or enemyPlayer.CharacterAdded:Wait()

	enemyCharacter.Humanoid.WalkSpeed = 28

	-- Transfer the dynamite
	-- Clone the dynamite in storage
	RemoveDynamite(player) -- Remove dynamite from the tagging player
	local SelectedDynamiteSkin = "Default_Dynamite" -- change this to the players actual selected dynamite skin later
	
	DynamiteSkinModule.ApplyDynamite(enemyCharacter, SelectedDynamiteSkin) -- Give enemyPlayer their selected dynamite skin

	-- Add ExplosionVFXPart (used to create explosionVFX)
	local rootPart = playerCharacter:FindFirstChild("HumanoidRootPart")
	if rootPart then
		for _, item in ipairs(rootPart:GetChildren()) do
			if item.Name == "ExplosionVFXPart" then
				return -- If ExplosionVFXPart exists in the HumanoidRootPart, do nothing
			end
		end

		-- If it doesnt exist then we create it
		local ExplosionVFXPart = game:GetService("ReplicatedStorage"):FindFirstChild("ExplosionVFXPart"):Clone()
		ExplosionVFXPart.Name = "ExplosionVFXPart"
		ExplosionVFXPart.Parent = rootPart
	end
end


-- When a player joins
game.Players.PlayerAdded:Connect(function(player)
	player.CharacterAdded:Wait()
	local character = player.Character or player.CharacterAdded:Wait()

	-- Add isTagged value
	local isTagged = Instance.new('BoolValue')
	isTagged.Name = 'isTagged'
	isTagged.Value = false
	isTagged.Parent = character

	-- Add Detonator Attribute (we use this later to make them detonator and checks etc)
	character:SetAttribute("DetonatorInfo", false) -- Set to false as default (not detonator)

	-- Add player to tag cooldown table
	cooldownInfo[player.UserId] = {
		Tagger = player.UserId,
		Time = 0
	}

end)

-- When the player leaves
game.Players.PlayerRemoving:Connect(function(player)

	-- Remove the player from the cooldown table
	if cooldownInfo[player.UserId] then
		cooldownInfo[player.UserId] = nil -- Remove them from the table
	end

end)

-- Fired from client, we do checks then fire AssignDynamite
local function TagPlayer(Tagger, enemyPlayer) -- We have the sending player (tagger) and the player that should be tagged
	if enemyPlayer and Tagger ~= enemyPlayer then
		local CurrentTime = tick()

		local enemyCharacter = enemyPlayer.Character -- Character of the enemy player getting tagged
		local enemy_IsTaggedValue = enemyCharacter and enemyCharacter:FindFirstChild("isTagged")

		local TaggerCharacter = Tagger.Character -- The taggers charatcer yeyeyeye
		local Tagger_IsTaggedValue = TaggerCharacter and TaggerCharacter:FindFirstChild("isTagged")

		if not enemyCharacter or not enemy_IsTaggedValue or not Tagger_IsTaggedValue then return end -- if enemy doesnt have character, istagged value and tagger doesnt have istaggedvalue then return end

		if cooldownInfo[enemyPlayer.UserId] then -- If the player exist in the table (doesnt mean that they have a cooldown)
			local LastTagTime = cooldownInfo[enemyPlayer.UserId].Time -- Gets the last time the player got tagged
			if CurrentTime - LastTagTime < CooldownDuration then -- If it has been less than 1 second since last tag then return nothing
				return
			end
		end

		if not enemy_IsTaggedValue.Value then -- If the enemy is not tagged
			Tagger_IsTaggedValue.Value = false -- We are setting our own tagged status to false
			enemy_IsTaggedValue.Value = true -- Making enemy tagged

			-- Remove highlight

			print(Tagger.Name, "is tagging", enemyPlayer.Name)
			AssignDynamite(Tagger, enemyPlayer) -- Fire to Assign them the dynamite
			InitiateTagHandlerEvent:FireClient(enemyPlayer) -- Make them able to tag (Initiate their tag ability on client)

			cooldownInfo[enemyPlayer.UserId] = { -- Setting new values inside cooldownTable for enemy (This is the new tagger)
				Tagger = Tagger.UserId, -- The player that tagged me
				Time = CurrentTime -- Last time I got tagged
			}
			cooldownInfo[Tagger.UserId] = { -- Setting new values for the old tagger inside cooldownTable. (This is the player that is not tagged anymore)
				Tagger = enemyPlayer.UserId, -- This is the player that we tagged
				Time = CurrentTime -- This is the time we tagged them
			}

			-- Fire to enemyClient
			DisplayTagUIRemote:FireClient(enemyPlayer, "gotTagged", Tagger) -- Fires to enemyPlrs client with the tagger that tagged them (us)
			DisplayTagUIRemote:FireClient(Tagger, "tagged", enemyPlayer) -- Fire to our client that we tagged someone else in our UI with the plr we tagged
		else
			print("Enemy player is already tagged.")
		end
	end
end

TagPlayerEvent.OnServerEvent:Connect(TagPlayer) -- Passes the player that should be tagged from client
1 Like

Hey, it’s kinda hard to tell what could be causing by only looking at the script, you should try opening the MicroProfiler, and looking for anything unusual, any spikes, or something exponentially growing, or going to Console → Memory, and check there for any high number that is unusual.

^

This is way better, and might show us the solution. Please read the article before replying please :slight_smile:

Alright so, I’m not very familiar with the Microprofiler. I tried pausing and comparing some functions and items taking up space but couldn’t find any real difference.

However here you can see the memory progressively getting more loaded every tag.

External Media

When it keeps going up and up, and you pause it with ctrl + p, you should be able to tell which one increased. Could you do it again, and pause it and show me everything you see in there?

Sure,
Here I paused it where it starts spiking up. What should I be looking at to see what is using all the memory?

External Media

On mobile. The first thing I would look at is the design. Can you get rid of runservice and use an event? The second thing I would look at is how many times makeplayerdetonator is called on a touch event

Hey, sorry for not answering, could you publish your game and send me the link so i can check the microprofiler out for myself?

Sure, just reminder that you need two clients to test the tag system :slightly_smiling_face:

What event were you thinking and why specifically on mobile? Because touch event felt unreliable to use.

Weird, i can’t see anything unusual happening in microprofiler nor do i have access to memory/luauheap, if i had to guess i would say it has something to do with the way ur connecting / disconnecting the renderstepped event, is there any particular reason why you would only want to connect it when the player gets the dynamite? You could just have it going for all players at a time, and then verify if the player has the dynamite or not, i would say the issue is with debounces, the console gets spammed with prints, so even if you have debounce on the server, the remote event will still fire, it will just get returned on the server, but since renderstepped runs 60 times per frame, it will probably fire the remote event multiple times, thus the call getting queued in the remote event invocation queue (this is just a guess, i’m not completely sure.)

There’s a memory leak with RunServiceConnection, you need to put either

if RunServiceConnection then
  return;
end

or

if RunServiceConnection then
 RunServiceConnection:Disconnect()
end

at the start of your InitiateTagHandler, otherwise when it’s called multiple times when isTagged is changed to true the original connection is overwritten and exists in memory

Sorry for the confusion. I was on mobile, just tried to prepare you for what could have been a poorly typed answer from a small mobile device. Yes, a touch event, is what i was thinking. All the code being run each frame is excessive, if you can scale it back to a filtered touch event, it would be more performant. Also, your dynamitePart.Touched event handler will probably be called multiple times, which will trigger MakePlayerDetonator multiple times and send the InitiateTagHandlerEvent to the client multiple times, which will start the heartbeat multple times and you get into the situation that Swag is commenting on.

I tried this but it only works once then breaks and tagging does not work anymore. Is there a better way than using this runservice. I would rather avoid runservice if possible since it is just causing problems.

Majority of devs would say .Touched is a bad way of making hitboxes. Its unrealiable and just bad but sure it’s simple. I tried using it before and it didn’t work very well at all. I don’t know about this RunService though. I want to scrap it since it’s just causing problems but I don’t know how else I would create a hitbox that constantly checks for another player and once a player is found fire the tag event etc.

Skimming through this, why are you calling this function again? It stacks an extra connection since it seems both InitiateTagHandlerEvent.OnClientEvent and isTagged.Changed gets fired simultaneously.

@Sniperkaos is also on the right track, except you set RunServiceConnection to nil after disconnecting. Do this for good practice:

-- whenever you are connecting to a signal
if someConnection == nil then
    someConnection = someSignal:Connect(function()
        -- do something
    end)
end

-- whenever you are disconnecting a connection
if someConnection ~= nil then
    someConnection:Disconnect()
    someConnection = nil
end

Appreciate the response,
I removed the extra call for the InitiateTagHandler function and added,

if RunServiceConnection then -- If there is a connection we disconnect it & set it to nil
		RunServiceConnection:Disconnect()
		RunServiceConnection = nil
	end

This improved the memory leak a ton! I tested continued tagging for like 2 minutes and the memory usage stayed the same. Until it suddenly broke and memory spiked and stayed like that. Not sure what caused that. Except for that memory leak occuring out of nowhere the problem is basically solved.

Question:
What do you mean by connecting to a specific signal? I notice you are connecting a specific signal instead of the entire connection. Where do you create that signal and what is its purpose?

It’s the double connection stacking. If you override a previous variable, it’s not going to automatically disconnect your old one. This pattern just prevents you from connecting a new one if the old one hasn’t been cleared yet.

Are you referring to someSignal? That’s just an example. RBXScriptSignals are signals like Changed, GetPropertyChangedSignal(), OnClientEvent, etc. and connections are RBXScriptConnections you connect with Connect, ConnectParallel, and Once.

Where is the “double connection” stacking/being created? I am just creating one connection but you mean the previous one is not disconnecting sometimes? I’m a bit confused. How would I make sure the old one disconnects in that case?

Whenever you called InitiateTagHandler in your old code. Look at the following pattern:

local function doImportantStuff()
    someConnection = RunService.Heartbeat:Connect(function(deltaTime)
        print(deltaTime, "has elapsed since last heartbeat")
    end)
end

someToggleValue.Changed:Connect(function()
    if someToggleValue.Value == true then
        doImportantStuff()
    else
        someConnection:Disconnect()
    end
end)

This looks fine right? Well, it’s identical to your previous code. However, your old code problem because when another RemoteEvent calling doImportantStuff again.

someRemote.OnClientEvent:Connect(function()
    doImportantStuff()
end)

Now you have 2 conditions that fire doImportantStuff, and the way the code was written, you aren’t disconnecting any old connections. This is why the pattern I mentioned above is important and you can basically go 2 routes to this:

local function doImportantStuff()
    -- disconnect before reconnecting.
    -- otherwise, someConnection will
    -- point to your new connection but
    -- the old one is still connected
    if someConnection ~= nil then
        someConnection:Disconnect()
        -- you dont have to set this to nil since
        -- it's being set directly after anyways,
        -- but it's good practice anyways for habit
        someConnection = nil
    end

    someConnection = RunService.Heartbeat:Connect(print)
end

-- or
local function doImportantStuff()
    -- checks if a connection is already connected.
    -- if it is, ignore the request entirely
    if someConnection == nil then
        someConnection = RunService.Heartbeat:Connect(print)
    end
end

-- if you have to disconnect outside of the doImportantStuff,
-- do the same pattern and check if it exists to prevent errors
if someConnection ~= nil then
    someConnection:Disconnect()
    someConnection = nil
end

PS: mark something here as solved if the issue is gone.