Instance streaming: the part was not destroyed, it was reparented to nil
Streaming comes switched on by default in a new place and cannot be changed from a script. The consequence that breaks code: when an instance streams out, it is reparented to nil — and the removal signals fire without anything having been destroyed.
Produced with AI from the sources cited below, and checked by automated verification against those same sources. No person read this text before publication. Editorial Policy

There is a streaming bug that appears in no Studio test and appears for every player: the code treats a part that went out of range as a part that was destroyed.
Both cases fire the same signal. And the first is reversible, while the second is not. This guide walks streaming from that distinction, because almost every bug on the subject comes out of it.
The problem
Instance streaming lets the engine load and unload 3D content from the Workspace dynamically. The gains the documentation lists are three, and they are real:
- Faster joins: the player starts in one part of the world while the rest loads in the background.
- Memory: the game runs on devices with less memory, because content comes and goes as needed.
- Performance: the server spends less time and bandwidth synchronising the world with players.
There is also level of detail, which keeps distant models and terrain visible even without being loaded — and SLIM delivers the highest fidelity of that for models and avatars.
Two sentences about control change the whole discussion. The first: streaming is controlled by the Workspace's StreamingEnabled property, and it comes switched on by default in a new place created in Studio. The second: that property cannot be set from a script.
Together, they mean that most recently created games have streaming on without anyone having decided that — and that there is no way to switch it off temporarily in code to work around a bug. Either the game works with streaming, or the setting changes in the editor.
Step by step
1. Understand the scope. Streaming applies exclusively to instances descending from the Workspace. Instances in other containers — ReplicatedStorage, ReplicatedFirst — are not eligible.
The documentation makes a point of stating the corollary, because it misleads: putting an atomic model in ReplicatedStorage does not guarantee atomic replication. A model's streaming properties mean nothing outside the Workspace.
2. Know what does not arrive on join. When the player joins, Workspace instances are replicated except: the BaseParts; models marked Atomic, Persistent or PersistentPerPlayer; Nonatomic models, which is the default, when ModelStreamingBehavior is Improved; and the descendants of all of that.
It is a short list that covers almost the whole scene of a game. In practice: the world is not there when your script runs.
3. Know how streaming out works. During the match, the client can remove regions of BaseParts, according to StreamOutBehavior. The process starts with the regions furthest from the replication focuses and works closer as needed.
There is a floor: regions within the StreamingMinRadius never leave. It is the minimum guarantee around the player, and it is where it is worth putting what cannot be missing.
Two important exemptions. A model with no descendant BasePart — like one used only as a container for scripts — is exempt from leaving, unless it becomes a descendant of a BasePart. And an instance created or cloned by a client script is exempt, unless it sits under an instance created by the server.
That second exemption is a tool: a visual effect the client creates stays where it is, as long as it is not hung on a part belonging to the server.
4. Know the exemptions as a tool. The two streaming-out exemptions are not a curiosity — they are the mechanism by which you guarantee something stays.
A purely organisational model, with no BasePart at all, stays where it is. That makes it the right place for scripts and values that have to exist all the time, regardless of where the player is. Hanging that same model inside a scenery part turns it into a candidate to leave along with it.
And what the client creates stays, as long as it is not hung on a server instance. A particle effect created locally and placed in the Workspace persists; the same effect placed inside the part that spawned it disappears when the part leaves.
The artifact
The module below tracks an instance that can come and go, and it exists to separate the two things the same signal does not separate.
onEnter runs every time the part is present — the first time and on every return. onLeave runs when it disappears, and it can run several times in the same session.
Notice what the usage example does not do in onLeave: it does not erase progress, it does not count the item as lost, it does not disconnect. The part can come back in seconds.
And notice what it does in onEnter: it reapplies the colour. That is not redundancy — it is the next section.
A note on how to test this, because it is the part that does not happen on its own: in Studio, with the whole map loaded and a single player, streaming almost never exercises the leaving path. The test that counts is walking to the far end of the map and back, watching what happens to the object your code tracks. If your map is not large enough for that, the problem has not gone away — it will just appear when the map grows.
Common mistakes
The central mistake sits in a sentence worth reading slowly: on streaming out, the instance is reparented to nil, so that existing Luau state can reconnect if it returns.
Two code-breaking consequences come out of that.
The first: removal signals like ChildRemoved and DescendantRemoving do fire on the parent or ancestor — but the instance is not destroyed in the same sense as a Destroy() call.
Code listening to ChildRemoved to conclude "the item was collected", "the enemy died", "the platform was removed" will conclude that every time the player walks far enough away. And it will conclude it again when they come back and walk away once more. On a full server, the same event fires dozens of times per session.
The test that tells them apart is Parent: streaming out leaves the instance alive with a nil Parent, while what was destroyed never comes back. In practice, the robust route is the artifact's — treating entry and exit as alternating states, rather than terminal events.
The second consequence is quieter: a property change that exists only on the client and was not replicated can be lost if the instance leaves and comes back.
That is the bug nobody finds. The client paints a part green to mark it as used, or hides an object the player collected locally. The player walks away, comes back, and the part is in its original state. There is no error, there is no log, and the behaviour depends on distance — which is the variable nobody thinks to test.
The fix is the artifact's: reapply the local state on every entry, instead of applying it once and trusting.
There is also a detail of order that saves debugging: since StreamingEnabled cannot be set from a script, there is no "streaming off just for this test" mode. The setting belongs to the place, and changing it to investigate a bug changes the behaviour of everything. The quickest route is usually the reverse — assume streaming is on and write client code already prepared for the part not being there.
Next step
The next subject is physics assemblies, because they follow their own rules and are where streaming meets performance.
Assemblies stream in as complete units, along with the associated Constraints and Attachments — which guarantees consistent physics on the client. The only exception is an anchored assembly, where only the BaseParts within the radius stream in. And they do not stream out until all of their BaseParts are eligible to leave.
From that comes a recommendation that is architectural, not about code: avoid a moving assembly with an unnecessarily large number of instances, because them all streaming in in unison can cause network and CPU spikes.
That changes how you build vehicles, moving platforms and large machines. A train of two hundred articulated parts is an assembly that streams in all at once, for every player who comes near — and the spike happens at the worst possible moment, which is when somebody is approaching at speed.
The practical exercise: list your game's moving assemblies and count the parts in each one. The largest are candidates to be simplified, split or anchored when stationary.
And, before all of that, the baseline check is worth doing: open your place and see whether StreamingEnabled is on. If it is — and in a new place it is — all the client code that assumes the Workspace is whole is already buggy code waiting for enough distance to show it.
Sources
Ready-to-use artifact
-- LocalScript: tracking an instance that streaming can take away and give
-- back, WITHOUT treating the departure as destruction.
--
-- The distinction this file exists to make:
--
-- STREAMED OUT -> Parent became nil, the object STILL exists,
-- and it can come back. Do not clear the state.
-- DESTROYED -> it never comes back. Then, and only then, clear.
--
-- Both fire ChildRemoved on the parent. Looking only at the signal does
-- not tell them apart.
local Watcher = {}
Watcher.__index = Watcher
-- `onEnter` runs every time the part is present and usable.
-- `onLeave` runs when it disappears from the client's world -- and it can
-- run several times over the session, alternating with `onEnter`.
function Watcher.new(parent, name, onEnter, onLeave)
local self = setmetatable({
parent = parent,
name = name,
onEnter = onEnter,
onLeave = onLeave,
current = nil,
connections = {},
}, Watcher)
-- ChildAdded catches both creation and the RETURN from streaming
table.insert(self.connections, parent.ChildAdded:Connect(function(child)
if child.Name == name then
self:_entered(child)
end
end))
table.insert(self.connections, parent.ChildRemoved:Connect(function(child)
if child == self.current then
self:_left()
end
end))
local already = parent:FindFirstChild(name)
if already then
self:_entered(already)
end
return self
end
function Watcher:_entered(instance)
if self.current == instance then
return
end
self.current = instance
if self.onEnter then
task.spawn(self.onEnter, instance)
end
end
function Watcher:_left()
local previous = self.current
self.current = nil
if self.onLeave then
task.spawn(self.onLeave, previous)
end
end
function Watcher:destroy()
for _, c in self.connections do
c:Disconnect()
end
self.connections = {}
self.current = nil
end
--[[
USE -- notice what is NOT here:
local lever = Watcher.new(workspace, "Lever",
function(part)
-- REAPPLY the local visual on every entry. A change that only
-- exists on the client is lost when the part leaves and comes
-- back, so it has to be redone here, not done once long ago.
part.Color = Color3.fromRGB(0, 255, 0)
end,
function()
-- Do NOT erase progress, do NOT count it as "the player lost the
-- item", do NOT disconnect the watcher. The part can come back
-- in seconds.
end)
]]
return Watcher


