Content Streaming

In-experience content streaming allows the Roblox engine to dynamically load and unload 3D content and related instances in regions of the world. This can improve the overall player experience in several ways, for example:

  • Faster join times — Players can start playing in one part of the world while more of the world loads in the background.
  • Memory efficiency — Experiences can be played on devices with less memory since content is dynamically streamed in and out. More immersive and detailed worlds can be played on a wider range of devices.
  • Improved performance — Better frame rates and performance, as the server can spend less time and bandwidth synchronizing changes between the world and players in it.
  • Level of detail — Distant models and terrain remain visible even when they're not streamed to clients, keeping the experience optimized without entirely sacrificing background visuals.

Enabling Content Streaming

Content streaming can be enabled through the StreamingEnabled property of the Workspace object in Studio (this property cannot be set in a script).

StreamingEnabled property enabled on Workspace object in Studio

Once enabled, it's recommended that you adhere to the following practices:

  • Because clients will not typically have the entire Workspace available locally, use the appropriate tool/API to ensure that instances exist before attempting to access them in a LocalScript. For example, use WaitForChild() on objects that may not exist, detect instance streaming, or utilize model streaming controls.
  • Minimize placement of 3D content outside of Workspace. Content in containers such as ReplicatedStorage or ReplicatedFirst is ineligible for streaming and may negatively impact join time and memory usage.
  • If you move a player's character by setting its CFrame, do so from a server-side Script and use streaming requests to more quickly load data around the character's new location.
  • Avoid parenting a part to another part, especially when the parent is far away from the child. Instead, group them as a Model or place them inside a Folder.
  • Manually set the player's ReplicationFocus only in unique situations such as in experiences that don't use a Player.Character. In these cases, make sure the focus is near the object(s) that the player controls to ensure content continues to stream in around the player's interaction point.

Technical Behavior

Streaming In

When a player joins, all instances in the Workspace are sent to the client, excluding BaseParts, atomic or persistent models, and descendants of those instances. Then, during gameplay, the server streams in BaseParts and their descendants to the client, as well as atomic and persistent models.


Instance sent to client when the experience loads

Instance sent to client when eligible for streaming in

Instance sent along with its parent

Diagram showing the Workspace with various child instances
Terrain is treated uniquely, in that the instance streams in when the experience loads, but terrain regions only stream in when needed

Streaming Out

During gameplay, a client may stream out (remove from the player's Workspace) regions and the BaseParts contained within them, based on the behavior set by StreamOutBehavior. The process begins with regions furthest away from the player's character (or ReplicationFocus) and moves in closer as needed. Regions inside the StreamingMinRadius range never stream out.

To further anticipate stream out, examine these scenarios:

ScenarioExampleStreaming Behavior
A part is created locally through Instance.new() in a LocalScript.In a "capture the flag" game, you create and attach blue helmet parts to all players on the blue team through a LocalScript.The part is not replicated to the server, and it is exempt from streaming out unless you make it a descendant of a part that exists on the server, such as a part within a player's character model.
A part is cloned locally from ReplicatedStorage through Instance:Clone() in a LocalScript.A wizard character casts a spell by activating a Tool, upon which an object including several special effects is cloned from ReplicatedStorage and parented to the workspace at the wizard's position.The part is not replicated to the server, and it is exempt from streaming out unless you make it a descendant of a part that exists on the server.
A part is reparented from ReplicatedStorage to the workspace via a LocalScript.A "wizard's hat" is stored in ReplicatedStorage. When a player chooses to play on the wizard's team, the hat is moved into their character model via a LocalScript.The part remains eligible for streaming out since it came from the server and was replicated to ReplicatedStorage. Avoid this pattern as it causes a desync between the client and server, and the part may stream out; instead, clone the part.

Assemblies and Mechanisms

When at least one part of an assembly is eligible for streaming in, all of the assembly's parts also stream in. However, an assembly will not stream out until all of its parts are eligible for streaming out. During streaming, all of the Constraints and Attachments descending from BaseParts and atomic or persistent Models also stream, helping to ensure consistent physics updates on clients.

Note that assemblies with anchored parts are treated slightly differently than assemblies with only unanchored parts:

Assembly CompositionStreaming Behavior
Unanchored parts onlyEntire assembly is sent as an atomic unit.
Anchored root partOnly the parts, attachments, and constraints needed to link the streamed parts to the root part are streamed in together.

Timing Delay

There may be a slight delay of ~10 milliseconds between when a part is created on the server and when it gets sent to clients. In each of the following scenarios, you may need to use WaitForChild() and other techniques rather than assuming that events and property updates always occur at the same time as part streaming.

ScenarioExampleStreaming Behavior
A LocalScript makes a RemoteFunction call to the server to create a part.A player activates a Tool locally to spawn a part on the server that all players can see and interact with.When the remote function returns to the client, the part may not yet exist, even though the part is close to the client focus and within a streamed area.
A part is added to a character model on the server via a Script and a RemoteEvent is fired to a client.When a player joins the police team, a "police badge" part stored in ServerStorage is cloned and attached to the player's character model. A RemoteEvent is fired and received by that player's client in order to update a local UI element.Although the client receives the event signal, there is no guarantee that the part has already streamed to that client.
A part collides with an invisible region on the server and triggers a RemoteEvent on the client.A player kicks a soccer ball into a goal, triggering a "goal scored" event. Other players that are close to the goal may see the "goal scored" event before the ball has been streamed to them.

Streaming Properties

The following properties control how content streaming applies to your experience. All of these properties are non-scriptable and must be set on the Workspace object in Studio.

Streaming properties shown on Workspace object in Studio

StreamingIntegrityMode

Your experience may behave in unintended ways if a player moves into a region of the world that hasn't been streamed to them. The Streaming Integrity feature offers a way to avoid those potentially problematic situations. Please see the StreamingIntegrityMode documentation for more details.

StreamingMinRadius

The StreamingMinRadius property indicates the radius around the player's character (or ReplicationFocus) in which content streams in at the highest priority. Care should be taken when increasing the default, as doing so will require more memory and more server bandwidth at the expense of other components.

StreamingTargetRadius

The StreamingTargetRadius property controls the maximum distance away from the player's character (or ReplicationFocus) in which content streams in. Note that the engine is allowed to retain previously loaded content beyond the target radius, memory permitting.

A smaller StreamingTargetRadius reduces server workload, as the server will not stream in additional content beyond the set value. However, the target radius is also the maximum distance players will be able to see the full detail of your experience, so you should pick a value that creates a nice balance between these.

StreamOutBehavior

The StreamOutBehavior property sets the streaming out behavior according to one of the following values:

SettingStreaming Behavior
DefaultDefault behavior, currently the same as LowMemory.
LowMemoryThe client only streams out parts in a low memory situation and may remove content until only the minimum radius is present.
OpportunisticRegions beyond StreamingTargetRadius can be removed on the client even when there is no memory pressure. In this mode, the client never removes content that is closer than the target radius, except in low memory situations.

Model Streaming Controls

As noted previously, the client of a joining player receives all instances in the Workspace, excluding BaseParts, atomic or persistent models, and descendants of those instances. Then, during gameplay, the server streams in BaseParts and their descendants to the client, as well as atomic and persistent models.

To avoid issues with model streaming and minimize use of WaitForChild(), you can customize how Models and their descendants stream through their ModelStreamingMode property.

ModelStreamingMode property indicated on a model's properties in Studio
SettingStreaming Behavior
Default / NonatomicWhen a default/nonatomic Model is streamed, its descendants are also sent, except for BasePart descendants.
AtomicAn atomic Model and all of its descendants are streamed in/out together. For streaming in, this applies when any descendant BasePart is eligible for streaming in. For streaming out, this applies when all descendant BaseParts are eligible for streaming out.
PersistentPersistent models are sent as a complete atomic unit soon after the player joins and before the Workspace.PersistentLoaded event fires. Persistent models and their descendants are never streamed out.
PersistentPerPlayerBehaves as a persistent model for players that have been added using Model:AddPersistentPlayer(). For other players, behavior is the same as Atomic. You can revert a model from player persistence via Model:RemovePersistentPlayer().

Default / Nonatomic

When a Model is set to Default or Nonatomic, its child Script instance will exist when the experience loads, but its child MeshPart and Part will not exist. This results in more instances that must be sent during loading, more instances stored in memory, and additional complexity for scripts that want to access the contents of the model. For example, a separate LocalScript will need to use WaitForChild() on the descendant MeshPart and Part.


Instance sent to client when the experience loads

Instance sent to client when eligible for streaming in

Instance sent along with its parent

Diagram showing the Workspace with a nonatomic model
LocalScript

-- Default/Nonatomic model exists at load time and is immediately accessible
local model = workspace.Model
-- Descendant parts do not exist at load time; use WaitForChild()
local meshPart = model:WaitForChild("MeshPart")
local part = model:WaitForChild("Part")

Atomic

If a Model is changed to Atomic, all of its descendants are streamed in together when a descendant BasePart is eligible. As a result, a separate LocalScript will need to use WaitForChild() on the model, but not on its descendant MeshPart or Part.


Instance sent to client when the experience loads

Instance sent to client when eligible for streaming in

Instance sent along with its parent

Diagram showing the Workspace with an atomic model
LocalScript

-- Atomic model does not exist at load time; use WaitForChild()
local model = workspace:WaitForChild("Model")
-- Descendant parts stream in with model and are immediately accessible
local meshPart = model.MeshPart
local part = model.Part

Persistent

Persistent models are not subject to normal streaming in or out. They are sent as a complete atomic unit soon after the player joins and before the Workspace.PersistentLoaded event fires. Persistent models and their descendants are never streamed out, but to safely handle streaming in within a separate LocalScript, you should use WaitForChild() on the parent model, or wait for the PersistentLoaded event to fire.


Instance sent to client when the experience loads

Instance sent to client when eligible for streaming in

Instance sent to client soon after player joins, exempt from streaming out

Instance sent along with its parent

Diagram showing the Workspace with a persistent model
LocalScript

-- Persistent model does not exist at load time; use WaitForChild()
local model = workspace:WaitForChild("Model")
-- Descendant parts stream in with model and are immediately accessible
local meshPart = model.MeshPart
local part = model.Part

PersistentPerPlayer

Models set to PersistentPerPlayer behave the same as Persistent for players that have been added using Model:AddPersistentPlayer(). For other players, behavior is the same as Atomic. You can revert a model from player persistence via Model:RemovePersistentPlayer().

Requesting Area Streaming

If you set the CFrame of a player character to a region which isn't currently loaded, streaming pause occurs, if enabled. If you know the character will be moving to a specific area, you can call Player:RequestStreamAroundAsync() to request that the server sends regions around that location to the client.

The following scripts show how to fire a client-to-server remote event to teleport a player within a place, yielding at the streaming request before moving the character to a new CFrame.

Script - Teleport Player Character

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)
LocalScript - Fire Remote Event

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local teleportEvent = ReplicatedStorage:WaitForChild("TeleportEvent")
local teleportTarget = Vector3.new(50, 2, 120)
-- Fire the remote event
teleportEvent:FireServer(teleportTarget)

Detecting Instance Streaming

In some cases, it's necessary to detect when an object streams in or out and react to that event. A useful pattern for streaming detection is as follows:

  1. Using a tool such as the Tag Editor, accessible from Studio's View tab, assign a logical CollectionService tag to all of the affected objects.

  2. From a single LocalScript, detect when a tagged object streams in or out through GetInstanceAddedSignal() and GetInstanceRemovedSignal(), then handle the object accordingly. For example, the following code adds tagged Light objects into a "flicker" loop when they stream in and removes them when they stream out.

    LocalScript - CollectionService Streaming Detection

    local CollectionService = game:GetService("CollectionService")
    local tagName = "FlickerLightSource"
    local random = Random.new()
    local flickerSources = {}
    -- Detect currently and new tagged parts streaming in or out
    for _, light in CollectionService:GetTagged(tagName) do
    flickerSources[light] = true
    end
    CollectionService:GetInstanceAddedSignal(tagName):Connect(function(light)
    flickerSources[light] = true
    end)
    CollectionService:GetInstanceRemovedSignal(tagName):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

Customizing the Pause Screen

The Player.GameplayPaused property indicates the player's current pause state. This property can be used with a GetPropertyChangedSignal() connection to show or hide a custom GUI.

LocalScript

local Players = game:GetService("Players")
local GuiService = game:GetService("GuiService")
local player = Players.LocalPlayer
-- Disable default pause modal
GuiService:SetGameplayPausedNotificationEnabled(false)
local function onPauseStateChanged()
if player.GameplayPaused then
-- Show custom GUI
else
-- Hide custom GUI
end
end
player:GetPropertyChangedSignal("GameplayPaused"):Connect(onPauseStateChanged)

Model Level of Detail

When streaming is enabled, Models outside of the currently streamed area will not be visible by default. However, you can instruct the engine to render lower resolution "imposter" meshes for models that are not present on clients through each model's LevelOfDetail property.

LevelOfDetail property indicated for Model instance
Model in actual level of detail
Actual model
Model represented as low resolution imposter mesh
Low resolution "imposter" mesh
Model SettingStreaming Behavior
StreamingMeshActivates the asynchronous generation of an imposter mesh to display when the model is not present on clients.
Disabled / AutomaticThe model disappears when outside the streaming radius.

When using imposter meshes, note the following:

  • Imposter meshes are designed to be seen at 1024 studs away from the camera or further. If you've reduced StreamingTargetRadius to a much smaller value like 256, imposter meshes may not be visually acceptable for the model they replace.
  • If a model and its descendant models are all set to StreamingMesh, only the top-level ancestor model is rendered as an imposter mesh, wrapping all geometries under the ancestor as well as its descendant models. For better performance, it's recommended that you use Disabled for descendant models.
  • Textures are not supported; imposter meshes are rendered as smooth meshes.
  • While a Model is not completely streamed in, the imposter mesh is rendered instead of individual parts of the model. Once all individual parts are streamed in, they render and the imposter mesh is ignored.
  • Imposter meshes have no physical significance and they act as non-existent with respect to raycasting, collision detection, and physics simulation.
  • Editing a model in Studio, such as adding/deleting/repositioning child parts or resetting colors, automatically updates the representative mesh.