Skip to content
Engine

Runtime

JunkService

Clean-up scopes for players, characters, rounds and instances, delayed destruction, and a leak detector that tells you what is piling up.

ServiceShared

Getting it

local JunkService = Engine:GetService("JunkService")

Overview

JunkService organises clean-up around scopes. A scope is a Bin with a name, a parent and counters: you add connections, instances, threads and callbacks to it, and destroying the scope cleans up all of them, including child scopes. JunkService hands out ready-made scopes for the lifetimes every game has.

Per-player clean-up without bookkeeping
local JunkService = Engine:GetService("JunkService")

Players.PlayerAdded:Connect(function(player)
	local scope = JunkService:GetPlayerScope(player)

	scope:Connect(player.Chatted, function(message)
		logChat(player, message)
	end)

	scope:Add(task.spawn(function()
		while true do
			task.wait(60)
			givePlaytimeReward(player)
		end
	end))
end)
-- When the player leaves, their scope is destroyed and all of the above stops.

The leak detector counts what every scope holds and warns you when a scope or a watched table keeps growing, which is usually the first sign of a memory leak.

Scopes

CreateScope

JunkService:CreateScope(name: string?, parent: Scope?): Scope

A new scope. With a parent, it becomes a child that is destroyed with the parent.

GetPlayerScope

JunkService:GetPlayerScope(player: Player): Scope

The scope for a player, created on first use. It is destroyed automatically when the player leaves.

GetCharacterScope

JunkService:GetCharacterScope(character: Model): Scope

The scope for a character model, created on first use. When the character is destroyed, JunkService forgets the scope so the next character gets a fresh one, but it does not destroy the old scope for you. Destroy it yourself (for example on Humanoid.Died), or use BindToInstance(character) when you want clean-up to happen automatically.

GetRoundScope

JunkService:GetRoundScope(): Scope

One shared scope for the current round. Destroy it at the end of the round; the next call creates a new one.

local function endRound()
	JunkService:GetRoundScope():Destroy() -- removes every round prop, effect and connection
end

BindToInstance

JunkService:BindToInstance(instance: Instance): Scope

A new scope that is destroyed when instance is destroyed.

SetDestroyDelay

JunkService:SetDestroyDelay(instance: Instance, seconds: number)

Destroys instance after seconds (if it still has a parent). Calling it again for the same instance replaces the old delay. Like Debris:AddItem, but cancellable and counted in the leak report.

Scope

MemberDescription
:Add(item, cleanupMethod?)Adds an item and returns it (same rules as Bin:Add).
:Connect(signal, fn)Connects and adds the connection.
:Remove(item), :RemoveNoClean(item)Take an item out, with or without cleaning it.
:Extend(name?): ScopeA child scope.
:AttachTo(instance): ScopeDestroy this scope when instance is destroyed.
:GetChildren(), :FindChild(name)Child scopes.
:GetStats(): StatsCounts for this scope and, recursively, its children.
:Clean()Cleans everything but keeps the scope.
:Destroy()Fires Destroying, cleans everything, destroys child scopes.
Name, Parent, Destroyed, SizeRead-only.
InstanceCount, ConnectionCount, GenericCountWhat the scope holds right now.
Destroying: SignalFires before the scope cleans up.

Leak detection

StartLeakWatch

JunkService:StartLeakWatch(config: { interval: number?, connectionThreshold: number?, instanceThreshold: number? }?)

Scans every scope on an interval and fires LeakDetected when a scope holds more instances or connections than the threshold, or a watched table grew by more keys than allowed since the last scan. You can also switch it on with JunkService.LeakWatchEnabled = true, which uses the configured defaults.

JunkService.LeakDetected:Connect(function(report)
	for _, suspect in report.Suspects do
		warn("Possible leak:", suspect)
	end
end)

JunkService:StartLeakWatch({ interval = 60 })

WatchTable

JunkService:WatchTable(t: table, label: string)

Adds a table of your own (a cache, a registry) to the leak scan. Watching does not keep the table alive.

GetReport

JunkService:GetReport(): LeakReport

Runs a scan right now and returns the report.

FieldDescription
TimestampWhen the scan ran (os.clock()).
TotalInstances, TotalConnections, TotalGenericTotals over every live scope.
ScopeCountNumber of live scopes.
PendingDestroysInstances waiting on SetDestroyDelay.
Scopes{ Name, InstanceCount, ConnectionCount, GenericCount } per scope.
WatchedTables{ Label, Size, GrowthSinceLastScan } per watched table.
SuspectsHuman-readable descriptions of what crossed a threshold.

LeakDetected

JunkService.LeakDetected: Signal<LeakReport>

LeakWatchEnabled

JunkService.LeakWatchEnabled: boolean

Whether the periodic scan is running. Off until you turn it on.

Configuration

Defaults from DefaultConfiguration/Services/JunkService4 settings
SettingDefaultWhat it does
DefaultLeakWatchInterval30Seconds between automatic leak scans.
DefaultConnectionThreshold500Per-scope connection count that flags a suspect.
DefaultInstanceThreshold1000Per-scope instance count that flags a suspect.
DefaultWatchedTableGrowthThreshold200Keys added to a watched table between scans that flags a suspect.

Types

export type Scope
export type LeakReport
export type LeakWatchConfig = { interval: number?, connectionThreshold: number?, instanceThreshold: number? }