Memory leak: Modulescripts never Garbage Collected after Destroy()'d and all references are removed

Reproduction Steps

I’ve created a test place to demonstrate this memory leak: (2) module GC test - Roblox

Basically, there’s a serverscript in ServerScriptService that is replicating a module over to the client constantly, a localscript in StarterPlayerScripts that is destroying all of the modules it receives.

Join the game and watch the Clientside Script memory (ignore server side, since I’m only deleting the modules on the client) increase at a constant rate as the new modulescripts flow in, but never go down even though the modulescripts are being destroyed and have no references to them that would prevent garbage collection.


^ Screenshot shows script memory rising

I added a button you can click to stop the server from sending over more modulescripts, so you can see that the deleted modulescripts are never cleared from Script memory clientside.


^ Observe the flat line on Script memory after no new modulescripts were replicated to the client, and all previously replicated modulescripts have been Destroy()'d and have no references to them (so they should be garbage collected, but aren’t)

[Sidenote: In my localscript code for deleting the modulescripts, I commented out a line where I require() the modulescript. If you uncomment that line, you can see that not only will Script memory rise without falling, but LuaHeap will also rise without falling. Initially, I encountered this issue through a memory leak in my game where LuaHeap kept rising, I narrowed it down to my system of loading in modulescripts packed with map data (the tables of Vector3’s you see inside the test modulescript I’m using). That’s how I came across this issue of modulescripts not being garbage collected, and I found that you can better observe this behavior through Script memory, since LuaHeap has more variability. But LuaHeap is also important to this issue.]

Expected Behavior

Memory should be cleared for ModuleScripts that are destroyed and have no references to them (they should be garbage collected)

Actual Behavior

Engine memory leak – Script memory (and LuaHeap if module is required) will rise but never fall when modulescripts are sent to the client and later deleted.

Issue Area: Engine
Issue Type: Performance
Impact: Very High
Frequency: Constantly

12 Likes

Hello and thank you for the report. I’m seeing this issue in a similar setup.

4 Likes

We have an issue right now where ModuleScript instances that are only destroyed on the client without a corresponding Destroy call on the server will remain in memory.
We don’t have a timeline for a fix of this issue right now.

As a work-around, try destroying the scripts on the server side when data is no longer required or use RemoteEvent to send data to clients instead.

5 Likes

Any news when the issue will be patched?

I can’t promise when this will be fixed, but we have people who started working on it recently.
It seems that the issue affects more than just scripts, but they hold much more memory than other instance types.

5 Likes

Do you have any ETA when it will be patched?

Just checked, no progress on this yet. I’ll raise the question about this internally one more time.

3 Likes

Still no news about that , sadly

Yes, at this point it’s fair to say that there’s no interest for fixing this issue.

I would suggest using a work-around I mentioned earlier.

3 Likes

thanks for the transparency! I’m guessing this is a byproduct of how the engine works and fixing it is complicated

1 Like

Hello again, got some unfortunate news on this.

After looking more into this issue, we have decided that we are not going to make a fix for this.

There is a problem with client-destroyed server-owned instances and references to them.
It is possible for the server to replicate a future object that references the ModuleScript that was earlier destroyed on the client and client can then use this new reference they got to clone the module and require it again.
For that to continue to work, ModuleScript has to be kept alive.
Similar situations can happen with other instance types as well.

I still recommend the workaround of destroying the instances on the server, so that the replication can take care of it on both sides.

6 Likes

This is leaning towards a feature request, but would it be possible to add a dedicated client-send method specifically for this use case of replicating an object to a specific client? The current setup to do this specifically is extremely cumbersome:

  1. Clone target object server-side, place target object in player’s PlayerGui (other local folders like Backpack should also work for this)
  2. Fire remote to client with reference to object in PlayerGui, delay for a frame and then clone that referenced object client-side (decouples the new cloned instance from server replication).
  3. Fire a remote back to the server to let it know that the client has received the object, server can now delete the original clone in PlayerGui.

The reason why this capability is important is for cases where you want to create custom streaming behavior. The alternatives are 1. just place all your map segments into replicatedstorage and let everything get streamed to all the clients (bad for client memory), or 2. Clone object into PlayerGui server-side and simply do not delete that copy; do not create a separate clone on the client; when the client is done with that instance and wants it deleted, then it tells the server to delete the object in PlayerGui (results in extra instances server-side, bad for server memory)

It’s been a while since I investigated this issue and maybe there’s a better workaround, but having a simple way to selectively replicate to a specified client would be extremely useful for creating custom streaming systems for cases (of which there are plenty) where Roblox’s built-in streaming doesn’t work as desired

4 Likes