Intermediate Scripter: How to Detect & Solve Memory Leaks?

As stated in the title, I want to know how to know if a script or a certain code-block of a script is causing a memory leak.

  • What are the different ways to detect a memory leak? If there are visual ways, how?

  • How can they be pinpointed to know which script / part of a script is causing them?

  • How can something be classified as a memory leak?

  • What are the different ways to deal with memory leaks?

For context, my previous game that I made has faced this problem: as the game proceeded to run longer, the different parts of the game started to lag noticeably!. Since I’m onto a new project, I want to be able to prevent this from happening again.

I saw this post (Best Practices Handbook) as I’m trying to increase my scripting proficiency and noticed this:


I knew then that this was the problem I encountered on my previous project.

How would I also know when to close the connection? This is one example:

-- For the quest Triggers
for i, questTriggerInstance in ipairs(questTriggers) do
    if questTriggerInstance:IsA("BasePart") then
        
        -- Connection
        questTriggerInstance.Touched:Connect(function(hit)
            local player = PlayersService:GetPlayerFromCharacter(hit.Parent) 
            if hit.Parent:FindFirstChildWhichIsA("Humanoid") and debounce[player.Name] == nil and PlayersService:FindFirstChild(hit.Parent.Name) then
                debounce[player.Name] = true
                initiateStartQuest(questTriggerInstance, player)
                task.wait(5) -- Wait before starting another quest
                debounce[player.Name] = nil
            end
        end)
    end
end

How can memory cleanup be used in this case? If memory leaks are indeed present.

I’ve seen different modules like Janitor but don’t know when and how to apply them.

3 Likes

If you want to close an event after it has occurred, you can simply use the Event:Once() that you had mentioned or look up tutorials on Janitor or Maid, but I’d recommend doing that for multiple events not just a single instance.

1 Like

How often does this code run?
Do you delete quest trigger Instances?

In your case you can just create connection and disconnect it as soon as someone has touched trigger

for i, questTriggerInstance in ipairs(questTriggers) do
    if questTriggerInstance:IsA("BasePart") then
        local connection
        connection = questTriggerInstance.Touched:Connect(function(hit) -- connection will be save in this thread
            local player = PlayersService:GetPlayerFromCharacter(hit.Parent) 
            if hit.Parent:FindFirstChildWhichIsA("Humanoid") and debounce[player.Name] == nil and PlayersService:FindFirstChild(hit.Parent.Name) then
                debounce[player.Name] = true
                initiateStartQuest(questTriggerInstance, player)
                task.wait(5) -- Wait before starting another quest
                debounce[player.Name] = nil
                connection:Disconnect()
            end
        end)
    end
end
1 Like

Any resources? I’ve been trying to look for them but all they are doing are kinda just showcases of those modules and how I need to use them but not how to use them.

Can you show me the full picture?
I just want to understand how often do you make those connections
And what if you have restarted quest? Can you restart the quest?
What about canceling the quest?

To use Janitor, all you really have to do is this:

local MyJanitor = Janitor.new()

-- Say imaginary part exists
Janitor:Add(Part.Touched:Connect(function()
    -- Your code here
    -- Cleanup and the method below is the equivalent of :Disconnect()
    MyJanitor:Cleanup()
end))

-- Alternatively,
MyJanitor()

To give more context, the MyJanitor:Add() is simply adding an event to a list of events inside the Janitor. There are three arguments in the method which is the object, method name, and the name. The method name i.e Stop or Play, is just a string and the Janitor will handle things, for example:

Obliterator:Add(Tween.new(0.5, 0, print), "Stop", "CurrentTween")

In the documentation, if you wrote this in your code this tween will stop or if you add an animation track with method name Play, the animation will play.

The name in the third argument really is just a name of the object, like an index per say. It’s like in a table recorded with lists and names.

If you need more information than this, ask anything.

1 Like

You can notice that memory in your game just skyrocket when you have memory leak like this:

local part = Instance.new("Part")
part.Parent =  workspace
part.Anchored = true
while task.wait() do
    part.Touched:Connect(function() -- this takes memory and it won't be released unless you delete part.
           print("lol")
    end)
end

You need to close connection when you:

  1. Don’t need that connection anymore
  2. Want to replace old connection with new one
2 Likes

This is very helpful since I could do (2.) using the logic you’ve provided which would hopefully prevent any memory leak for the code I gave as an example:

By the way, for the local connection variable, couldn’t I just declare it on the scope where all subsequent code-blocks can access it (but I think multiple code-blocks might change it at the same time) ? Like this:

local connection
-- [[Other variables and functions which 
--would be accessible anywhere within the script]]

for i, questTriggerInstance in ipairs(questTriggers) do
    if questTriggerInstance:IsA("BasePart") then
        connection = questTriggerInstance.Touched:Connect(function(hit) -- connection will be save in this thread
          -- Same logic
            -- Same logic

                connection:Disconnect() -- Main point in focus
            end
        end)
    end
end

Or should I just declare that variable on that specific scope instead, i.e., not accessible in other blocks.

BTW, Thanks a lot for your replies! You’ve been helping me wrap my head around this easily!

It’s probably better to keep it within the specific scope as the previous event will be lost forever in reference, and may cause leaks cause the global connection variable keeps changing.

1 Like

Also the easiest way to check memory leak is to check this thing
check this out!


My computer start lagging slowly after making MemoryLeak!

1 Like

All of these replies have been pretty relevant. Knowing when connections need to be disconnected can be confusing at first. Not all connections need to be disconnected. If you want a Touched event to run throughout the entirety of your game, there is no reason to disconnect it. Disconnect events that are no longer needed and still retain memory.

It’s also worth mentioning that if your connection is connected to a part (ex: Part.Touched, Part.Changed, etc.), you can destroy the part to garbage collect all events linked to it. This includes destroying parents/ancestors of the part. If you wanted to make a .Touched event only for a specific part, and then delete the part when your done; You don’t need to clean up any events.

Regarding connection helpers, I usually use Maid or Trove, or a combination of both. They are pretty similar in their functionality. But, you have to decide when to use them. Don’t use connection helpers for a script that involves disconnecting one or two events. Just disconnect those events like you normally would. There is no point in using Maid or Trove for small tasks.

1 Like

There are a lot of potential causes for memory leaks, but I’ll detail a few major ones.

  • Players and characters.

See, players and characters don’t actually get destroyed when they leave or respawn - their parents are just set to nil.

This means they will indefinitely stay in memory - over time, a server can get saturated with these connections and experience degradation.

This behavior will change somewhere this year though, just keep this in mind.

  • Being inexperienced.

This is something that others here have already mentioned, but please disconnect or destroy instances that you know you’re done with.

Don’t make unnecessary references to those instances too, or the GC won’t clean them up for you.

Also keep in mind that destroying instances also disconnect any events connected to them.

Eg: if you have a treasure chest that opens when someone holds down a button near it with a ProximityPrompt, you can probably just destroy the prompt, or at least disconnect the prompt event after the player is done with it. Or even better, just destroy the chest after a while.

Understanding these will help you understand the patterns behind what causes memory leaks, and how you can fix them.

Just search up on any GC tutorial on Roblox, and you’ll find loads of guides for them.

1 Like

Also to add on,

No, not all memory leaks will result in a huge spike in memory.

A gradually increasing memory usage can still be indicative of a memory leak.

It also applies vice versa - just because there is a huge spike in memory, doesn’t mean there is a memory leak.
Maybe you just required a huge module that might take up a lot of memory, or you have just loaded in a huge map into your workspace.

2 Likes

This is exactly the reason my previous game experience a whole ton of lag after 10-20 minutes of playing and players killing each other with magic. My cleanup of the parts that were used for the magic VFX etc. were good and responsive, but I didn’t know that I would also have to cleanup the characters after respawn which clogged up memory after some time! Especially with the modifications I did with the character like adding different stuff.

Thanks a lot!

These kinds of topics should be discussed more and be put in practice early on for the new scripters! Browsing through devforum and youtube, there aren’t much friendly introductory forums related to clean-ups (and not really much videos about it). Most are too complex to understand especially with jargons.

Nevertheless, learning to utilize those modules— in the correct cases (for bigger cleanups and not small tasks, as you’ve stated)— will surely be of big help for those like me who wants to be able to code up to industry-standard!

Just to add on,

You can prevent this from happening by setting workspace.PlayerCharacterDestroyBehavior to true.
This forces player and character instances to be destroyed once they respawn or leave.

This will be opt-out and eventually default behavior somewhere in the future, but as it stands, it’s opt-in, so you’ll have to enable it yourself.

Please do this - it’s a really big problem for server longevity for practically every Roblox game in existence.

CharacterRemoved and PlayerRemoving will still work as per normal, though if you have any deferred tasks within those events, they may fail as the instances may be destroyed.

or you can just do what i do and come up with your custom character spawn behavior looooool

2 Likes

I tried recreating this and would like to know, is MemoryLeak the name of the script you’ve created:

EDIT a few mins later:
Found the MemoryLeak script. The RunContext was on the ‘Server’ so I needed to make set the LuauHeap on the server as well.

Also, I’m not sure why but I have these empty slots but still seems to consume memory?

EDIT 10 mins later:
I have learned how to somewhat use the new LuauHeap feature:

Here’s what I did:

  • Create an initial snapshot of the memory allocation (in the server since the MemoryLeak script is a ServerScript
  • Run it through by doing the .Touched event
  • Create another Snapshot and Compare those two snapshots.

This answers my question:

If you have event listeners or loops that aren’t created at the start of runtime, make sure to close them. Loops can be broken with break or return, and listeners can be ended by MyFunction:Disconnect().

A trick that can be used to replace old loops is to use newproxy(), which generates a unique but otherwise empty value. At the start of each iteration, a loop can check if a newproxy value has been updated, and if it has, it will self-terminate.

Example:

PL_Proxy = newproxy()
function PrintLoop(PL_String)
	local CheckProxy = newproxy()
	PL_Proxy = CheckProxy
	while true do
		if CheckProxy ~= PL_Proxy then break end
		print(PL_String)
		task.wait(1)
	end
	print("Ending print loop "..PL_String)
end

1 Like

So in conclusion you should disconnect connections inside for loops?

1 Like