16 min read

Source: Roblox Creator Hub · CC BY 4.0 · View source · Code samples: MIT Imported 2026-10-03. Formatting adapted for this site.

Techniques and conversion

This guide outlines several techniques for using in-game instance streaming efficiently and effectively. While there's no "one size fits all" solution for designing a streaming game, following these high‑level steps will get you most of the way there.

Note

If you are converting a non-streaming game to streaming, be sure to check out the AI streaming conversion skill which can convert games to streaming automatically.

Streaming properties

Once StreamingEnabled is toggled on for the Workspace object in Studio, set its related properties to the following recommended values:

Property Recommendation
[`EnableSLIMAvatars`](/docs/workspace#workspace-enableslimavatars) Use [`Enabled`](/docs/enum-rolloutstate#enabled) to render R15 avatars as lightweight, animated stand‑ins when appropriate. See [SLIM avatars](#slim-avatars) for more info.
[`ModelStreamingBehavior`](/docs/workspace#workspace-modelstreamingbehavior) Use [`Improved`](/docs/enum-modelstreamingbehavior#improved) to enable the most efficient streaming for [`Models`](/docs/model) with [`BasePart`](/docs/basepart) descendants.
[`StreamingIntegrityMode`](/docs/workspace#workspace-streamingintegritymode) Use [`PauseOutsideLoadedArea`](/docs/enum-streamingintegritymode#pauseoutsideloadedarea) to balance gameplay integrity without pausing unnecessarily or too often.
[`StreamingMinRadius`](/docs/workspace#workspace-streamingminradius) Use the default of `64` to maximize how much the engine can scale the game down for low‑end devices.
[`StreamingTargetRadius`](/docs/workspace#workspace-streamingtargetradius) Use the default of `1024` to strike a good balance between visibility for players on high‑end devices and a reasonable memory footprint.
[`StreamOutBehavior`](/docs/workspace#workspace-streamoutbehavior) Use [`Opportunistic`](/docs/enum-streamoutbehavior#opportunistic) to allow the client to aggressively garbage collect content, significantly reducing memory usage and helping prevent out‑of‑memory crashes.

Model level-of-detail

Model.LevelOfDetail helps fill in non‑streamed Model content with lightweight composite or imposter meshes, making the world look visually complete. SLIM (Scalable Lightweight Interactive Models) are particularly effective, as players often cannot distinguish a SLIM mesh from the fully streamed‑in original.

For best results:

Disabled/Automatic

	<img src="https://prod.docsiteassets.roblox.com/assets/optimization/streaming/SLIM-Enabled.jpg" />

SLIM

Model structure

Beyond setting model level‑of‑detail, the structure and settings of your Models has a significant impact on how well streaming performs. As you build out or convert an existing game:

Note

Atomicity guarantees apply only during initial replication. Once an atomic model has been replicated to the client, any **new** instances added under that model are **not** replicated atomically and are instead streamed according to normal [streaming behavior](/docs/roblox-workspace-streaming#technical-behavior). Systems that dynamically modify atomic models must handle late‑arriving instances explicitly where necessary.

SLIM avatars

Platform avatars outside of the currently streamed area are not visible by default, but enabling Workspace.EnableSLIMAvatars renders R15 avatars as lightweight, animated stand‑ins when appropriate. Effectively, the engine:

SLIM avatars support R15 player characters with a body, dynamic head, standard or advanced R15 rig, and rigid/layered accessories. R6 avatars, NPCs, and avatars with custom proportions are excluded. For the full list of supported and excluded avatar configurations, performance data, and troubleshooting tips, see SLIM avatars.

Script patterns

The following script patterns are most commonly affected by streaming. The correct strategy depends on the intent of the code, so each pattern lists multiple options where appropriate.

Direct index to descendants

Indexing into Workspace descendants with the . operator throws an error if any instance in the path isn't currently streamed in. The same applies to FindFirstChild(), FindFirstChildWhichIsA(), and FindFirstChildOfClass() which return nil if the child has not streamed in.

local house1 = workspace:FindFirstChild("House1") -- nil if "House1" has not streamed in
local door = workspace.House1.Door -- Broken if "House1" or "Door" has not streamed in

A similar pattern is accessing the Humanoid or other character descendants directly inside a Player.CharacterAdded connection. Under streaming, the character model is parented to Workspace before all of its descendants have replicated, so direct indexing fails.

local Players = game:GetService("Players")

local player = Players.LocalPlayer

player.CharacterAdded:Connect(function(character)
	local humanoid = character.Humanoid
end)

Strategy #1

If the script cannot make progress without an instance, wait for it with WaitForChild():

local house1 = workspace:WaitForChild("House1")
local door = house1:WaitForChild("Door")

Strategy #2

When an instance may not be streamed in but the script can proceed without it, nil‑check with Instance:FindFirstChild().

local house1 = workspace:FindFirstChild("House1")
local door = house1 and house1:FindFirstChild("Door")
if door then
	door.Color = Color3.new(1, 0, 0)
end

Strategy #3

Make the parent a Model, set its ModelStreamingMode to Atomic, and place objects like Door as descendants. The engine guarantees all of the initial descendants are present once the atomic model itself is available, but you'll still need to use WaitForChild() on the model itself, as well as instances added to the model after it's been replicated.

local house1 = workspace:WaitForChild("House1")

-- House model is atomic; its initial descendants are guaranteed to be present
local door = house1.Door

Instances sent remotely

A RemoteEvent/RemoteFunction signal and the instance it refers to travel independently, so the signal can arrive on the client before the instance is present — or the instance may never be present at all. Two likely causes include:

Strategy #1

If the receiving client script needs the instance to proceed, include WaitForChild() before using it. Note that this can yield indefinitely if the instance never streams in, so consider adding a timeout as the second parameter of WaitForChild().

Strategy #2

Pre‑fetch the area with Player:RequestStreamAroundAsync() before firing the event. This makes it likely (though not guaranteed) that the instance is present when the client receives the event. See proactive streaming.

Strategy #3

If the script can tolerate the instance being absent for some clients, simply nil‑check with Instance:FindFirstChild() and skip the work for those clients.

Client desynchronization

Client‑side desynchronization should be treated as an exception, not a standard design pattern. Introducing client‑only copies or reparenting instances locally can create serious issues. Audit your code for places which rely on these types of changes persisting on the client.

For example, reparenting an instance locally from ReplicatedStorage to Workspace can make that instance eligible to be streamed out. Similarly, cloning an instance locally (Instance:Clone()) from ReplicatedStorage into Workspace creates a client‑only copy that is no longer part of the server replication pipeline and will not receive property updates from the original server‑owned instance.

The same concept applies when calling Instance:Destroy() on the client for a server‑owned object. This removes the instance locally but the server still has it, so it will stream in again with its original state when eligible.

Proactive streaming

When a player's next destination can be anticipated, make server‑side calls to Player:RequestStreamAroundAsync() to stream in transient areas for temporary loading, or use Player:AddReplicationFocus() on a limited basis for areas that should remain loaded until explicitly released.

For example, when a player character is about to teleport by CFrame change to another player's house in a distant location, you can pre‑fetch the destination area to minimize pop‑in and provide a smoother transition. The following script shows how a client-to-server remote event can be fired to move a player character using a pre-fetching method. If the pre-fetch request is successful when the function returns, the minimum radius around the target location should be present on the client.

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local teleportEvent = ReplicatedStorage:WaitForChild("TeleportEvent")

local function teleportPlayer(player, teleportTarget)
	-- Request streaming around target location
	player:RequestStreamAroundAsync(teleportTarget)

	-- Teleport character
	local character = player.Character
	if character and character.Parent then
		local currentPivot = character:GetPivot()
		character:PivotTo(currentPivot * CFrame.new(teleportTarget))
	end
end

-- Call teleport function when the client fires the remote event
teleportEvent.OnServerEvent:Connect(teleportPlayer)

Instance property reads

Once an instance streams out, its property updates are no longer replicated to that client. Reading properties such as BasePart.Position continue to succeed but return the last replicated value which can be arbitrarily stale.

local Players = game:GetService("Players")

local player = Players.LocalPlayer

-- Position may be stale if "target" has streamed out
local dist = (target.Position - player.Character.HumanoidRootPart.Position).Magnitude

Strategy #1

Move the logic to the server, as server‑side scripts see all instances at all times. This is generally the most reliable option for distance checks and other position‑sensitive logic.

Strategy #2

Detect stream out using signal change detection, or test Instance:FindFirstAncestorWhichIsA("Workspace") as false to skip the stale property check for instances no longer present.

Strategy #3

If the part must always be available, group it inside a Model and set the model's ModelStreamingMode to Persistent. Use this approach sparingly, as persistent models never stream out and permanently occupy memory.

Waiting on the critical path

Some non‑streaming games load their map by cloning it from ReplicatedStorage into Workspace, then wait for it on the client before dismissing a loading screen and signaling readiness. Under streaming this hangs indefinitely — the client's character hasn't spawned yet, so there's no replication focus, and the spatial map instance never streams in.

Strategy #1

Move the loading screen logic so that it doesn't depend on a specific spatial instance being present, for example by signaling readiness once the character has spawned and the immediate surrounding area is streamed in.

Strategy #2

Call Player:AddReplicationFocus() at the desired spawn location to give the client a replication focus before the character spawns.

Signal change handling

Signals such as Instance.ChildAdded/Instance.ChildRemoved and CollectionService signals like GetInstanceAddedSignal() or GetInstanceRemovedSignal() also fire on stream in/out, indistinguishable from real spawns/removals. Receiving scripts can't tell the difference from the signal alone, so logic which assumes a signal corresponds to a "real" event needs to be updated.

Strategy #1

Audit scripts for any signal listeners that could break or change significantly when triggered by stream in and/or stream out. For example, if you play audio or visual effects when an enemy NPC initially spawns into the world, assign each enemy an attribute such as Spawned on the first spawn, and skip replaying the same audio/effects in future stream‑ins of the enemy.

local CollectionService = game:GetService("CollectionService")

local TAG_NAME = "Enemy"

CollectionService:GetInstanceAddedSignal(TAG_NAME):Connect(function(enemy)
	if not enemy:GetAttribute("Spawned") then
		-- Set "Spawned" attribute on enemy for initial spawn
		enemy:SetAttribute("Spawned", true)

		-- Play audio/visual effects for this initial spawn
		playSpawnEffects(enemy)
	end
end)

Strategy #2

Assign a logical CollectionService tag to all of the affected objects using AddTag() or the Tags section of an instance's properties in Studio. Then, from a client‑side script, detect when a tagged object streams in or out through GetInstanceAddedSignal() and GetInstanceRemovedSignal() and handle the object accordingly.

local CollectionService = game:GetService("CollectionService")

local TAG_NAME = "FlickerLightSource"

local random = Random.new()
local flickerSources = {}

-- Detect tagged parts currently streamed in
for _, light in CollectionService:GetTagged(TAG_NAME) do
	flickerSources[light] = true
end

-- Detect new tagged parts streaming in or out
CollectionService:GetInstanceAddedSignal(TAG_NAME):Connect(function(light)
	flickerSources[light] = true
end)
CollectionService:GetInstanceRemovedSignal(TAG_NAME):Connect(function(light)
	flickerSources[light] = nil
end)

-- Flicker loop
while true do
	for light in flickerSources do
		light.Brightness = 8 + random:NextNumber(-0.4, 0.4)
	end
	task.wait(0.05)
end

Iteration over collections

Client-side collection iterations like Instance:GetChildren() and Instance:GetDescendants() return only the streamed‑in subset of descendants. This applies even when the parent itself is always replicated, such as a Folder directly under Workspace whose spatial descendants stream in and out.

local Players = game:GetService("Players")

local player = Players.LocalPlayer

-- Folder "Homes" is always replicated but its children stream in and out
-- This loop may miss homes that aren't currently streamed in
for _, home in workspace.Homes:GetChildren() do
	if home.Settings.Owner.Value == player.Name then
		return home
	end
end

Strategy #1

If a complete enumeration is required, perform the scan on the server and pass the result to the player through a RemoteEvent if needed.

Strategy #2

If the anticipated enumeration set is small enough, group each descendant as a Model and set ModelStreamingMode to PersistentPerPlayer. This is appropriate for things like a roster of player‑owned plots that's small and game‑critical.

Strategy #3

If the script's purpose is to find the nearest match or operate on whatever's currently visible, partial results may be acceptable. Comment on this assumption inside the script so that other maintainers don't reintroduce it as a bug.

Spatial queries

Client‑side spatial queries like WorldRoot:Raycast(), WorldRoot:GetPartBoundsInBox(), and Model:GetBoundingBox() reflect only streamed‑in content. Whether that's a problem depends on what the query is used for.

Strategy #1

Use the server for queries whose result must reflect the full world, for example a raycast that checks whether the player has line of sight to a distant target.

Strategy #2

If the query is intentionally local, for example a small radius around the player, it's already operating on streamed content and no change is needed.

Strategy #3

To spatially query a complete Model, set its ModelStreamingMode to Atomic. An atomic model is guaranteed to have all of its initial descendants present once the model itself is available, so bounds and extents will be accurate.

Strategy #4

If a model's geometry is known at edit time, store its bounds as attributes on the model itself and read those instead of recomputing on the client.

Other patterns

The following patterns also may also apply and should be carefully considered:

Realistic test conditions

Once scripts are updated, test the game thoroughly. Streaming bugs often only manifest at the edges of the streamed area or during transitions, so only testing near spawn or at target radius isn't sufficient.

AI streaming conversion skill

To assist with streaming conversion and optimization, Roblox offers an AI streaming skill that evaluates your game, applies recommended configurations, and cleans up compatibility issues, including:

Model & World Adjustments

Script & Replication Fixes

Built-in Guardrails

Note

Even if your game already utilizes streaming, the skill will scan your existing setup, identify potential issues or areas for improvement, and automatically apply fixes to optimize it further.


To use the AI skill in your game:

Back up your game. The conversion process can be complex, so you should always save a backup (File ⟩ Publish to Roblox As) before running the skill.

  1. You can run this skill directly in Assistant or using any LLM you prefer through the Model Context Protocol (MCP) in Studio. Higher-end AI models with large context windows are recommended; for a typical conversion, a frontier model might take 20-30 minutes and utilize roughly 200,000 tokens of context.

    1. Enable and connect the MCP server in Studio.
    2. Open your game in Studio.
    3. Open Assistant Settings ⟩ Skills and ensure that rbx-convert-to-streaming is toggled on.
    4. Prompt Assistant (or your LLM connected via MCP) to use the skill.