Your catalog has 100 items registered, but only the one you select appears on screen. To show that one, do you need to load models you have not selected?
Soft references are how you organize that loading. You remember where an asset lives and load it once you decide to use it. Add async loading and other game work continues while you wait for it to finish.
This article builds a small display piece where a placeholder box becomes a cone as you approach. Get a feel for "load later and change appearance from the delivered result", then apply it to the heavy models in your own game.
What You'll Learn
- How hard and soft references change when loading happens
- Receiving Async Load Asset's completion and swapping the mesh
- Duplicate requests while loading, and handling failure
- Investigating reference relationships and runtime memory separately
- Remember the asset's "location"
- Async means receiving completion later
- Hands-On: a display piece whose box becomes a cone
- Request the load once you approach
- Apply the mesh that arrives
- Confirm success and failure
- Check whether you really load less
- Load shortly before use, and release when done
- Bonus: Good to Know Up Front
- Summary
Remember the asset's "location"
A reference is what specifies the target you want to use. Set a cone asset on a Static Mesh variable and that variable names the same cone. Assets here are the materials saved in the Content Browser: meshes, images, sounds.
A hard reference chains the loading
Making a Blueprint's mesh variable an Object Reference and saving a specific asset as its default creates a hard reference to that asset. Loading that Blueprint loads its required references too.
That chain can continue past its targets. The display Blueprint uses a mesh, the mesh uses a material, and that material uses textures. Holding every display candidate as a hard reference causes candidates you never select to load.

A Soft Object Reference , meanwhile, records the asset's path . It remembers an address like /Game/AsyncDemo/Meshes/SM_AsyncDemo and does not load the mesh from that reference alone.
| Point of comparison | Hard reference | Soft reference |
|---|---|---|
| Loading the side that stores it | Loads the target as well | Does not load the target from that reference alone |
| Using the asset | Names an already loaded target | Requests a load and uses the result you receive |
| When to choose it | Used from the start, must not wait | Used later, or possibly not at all |
A soft reference is not a mechanism for eliminating loading; it is a mechanism for choosing when to load. If another Blueprint or level hard-references the same mesh, it can be loaded from there. Making one variable a soft reference does not guarantee the asset stays unloaded.
Object and Class differ in what they select
We load a "cone mesh" asset here, so we use a Static Mesh Soft Object Reference. If instead you want to load an enemy Blueprint later and Spawn it, use a Soft Class Reference . A Class specifies "which kind of Actor to create".
| What you want to use later | Reference kind | Loading node |
|---|---|---|
| Assets like meshes, images, sounds | Soft Object Reference | Async Load Asset |
| Blueprint classes such as Actors to Spawn | Soft Class Reference | Async Load Class Asset |
Loading a class and spawning an Actor are separate steps. For the latter, pass the loaded Class to Spawn Actor from Class .
Async means receiving completion later
Synchronous loading waits in place until loading finishes before continuing. Load Asset Blocking is that. Reading a large asset during play makes one frame long and causes the screen to hitch.
Asynchronous loading requests the load and receives the result later. With Async Load Asset , the continuation runs from the Completed exec output when loading finishes.

| Async Load Asset pin | Role |
|---|---|
| Asset | The soft reference you want loaded |
| The upper regular exec output (Out) | Work that continues without waiting. Unused in this hands-on |
| Completed | Continues to work that uses the load result |
| Object | Receives the loaded asset. Not a valid result on failure |
Put the mesh-swapping logic after Completed . Wiring it to the upper exec output would try to use a result that has not arrived.
How long you wait depends on the asset's size, the runtime environment, and whether it is already loaded. Sometimes it is instant; sometimes it takes time. Rather than deciding "wait N seconds", use Completed as the signal.
Also, going async does not remove the cost. Initializing a large model after loading, or updating many displays at once, can still be heavy. Think of it as changing how you wait, and then measuring the real cost .
Hands-On: a display piece whose box becomes a cone
From here we use the Blueprint Third Person template. Entering the area near the display starts loading, and on success the box swaps to a cone. On failure the box remains and a failure message prints.

A cone is light, so it is fine if no visible wait or performance difference appears. We first confirm that a completion notification can correctly change the appearance.
Prepare the asset to load
Turn on "Show Engine Content" in the Content Browser settings and find Cone under /Engine/BasicShapes . Copy it into your project's Content/AsyncDemo/Meshes and name it SM_AsyncDemo . When you drag it into the folder, choose copy rather than move.
Using this duplicate lets you find your own test mesh by name later in Reference Viewer and measurement results. You do not need to place SM_AsyncDemo in the level directly.
The display piece and the range that detects approach
Create BP_AsyncDisplay with Actor as its parent. In Components, add these two as children of DefaultSceneRoot.
| Name | Component | Settings |
|---|---|---|
DisplayMesh | Static Mesh | Mesh = /Engine/BasicShapes/Cube , Location = (0, 0, 50) , Mobility = Movable , Collision Presets = NoCollision |
LoadZone | Box Collision | Location = (0, 0, 100) , Box Extent = (200, 200, 150) , Collision Presets = OverlapOnlyPawn , Generate Overlap Events on |

DisplayMesh is the component that shows a shape on screen. Assign the Cube at first as a placeholder until loading finishes. Movable means it can change during play, and Overlap notifies that ranges intersected instead of blocking.
Box Extent is the distance from the box's center to each face. Our range becomes 400 cm wide, 400 cm deep, and 300 cm tall. Place it a little away from Player Start so you can see the box before entering the range.
Prepare the soft reference and two states
Create these variables in "My Blueprint". For TargetMesh, search for Static Mesh in the type picker and set the reference kind to Soft Object Reference . That is a different type from Static Mesh Component.

| Variable name | Type | Default | Meaning |
|---|---|---|---|
TargetMesh | Static Mesh Soft Object Reference | SM_AsyncDemo | Where the mesh to load later lives |
bBusy | Boolean | false | A load is currently pending |
bApplied | Boolean | false | The loaded mesh has been applied |
Compile after creating the variables, then choose SM_AsyncDemo as TargetMesh's default. We only try one kind here, so leave Instance Editable off.
Another key point: do not assign SM_AsyncDemo to DisplayMesh from the start. Even with TargetMesh as a soft reference, setting it directly on the display component makes that a hard reference.
Request the load once you approach
Right-click in the Event Graph and create RequestDisplayMesh from "Add Custom Event". No inputs needed. This event is the entry point for the loading logic.
We build it as a Custom Event on the Event Graph . Do not try to put logic that receives completion later, like Async Load Asset, inside a regular Function.
Call it only when the player enters
Select LoadZone and add On Component Begin Overlap from the events section in Details. Compare Other Actor against Get Player Character 's Return Value (Player Index = 0) with Equal (Object) and connect the result to a Branch's Condition.

Connect a white exec wire from the same Overlap event to the Branch and call RequestDisplayMesh from the True side. Connect the Equal (Object) Return Value to that Branch's Condition. Target is Self, meaning this BP_AsyncDisplay itself. Leave the False side unconnected.

Now only the template's player entering advances to your loading event. This is a single-player experiment. Search for and place the call node with the same name as the event, distinguishing it from the red Custom Event entry.
Exclude loading-in-progress and already-applied
Place a Branch from RequestDisplayMesh's entry. Feed Gets of bBusy and bApplied into OR Boolean and connect the result to Condition.

OR returns true if either is true. If loading or already applied, the Branch's True side ends without doing anything. Connect the False side to Set bBusy (true), then a Print String showing Loading... .

bBusy is the marker preventing duplicate requests while waiting. bApplied is the marker preventing repeated swaps after success. Recording "requested" and "done" separately also makes failure handling clearer.
Apply the mesh that arrives
Place Async Load Asset after the Print String and connect a Get of TargetMesh to Asset. Leave the upper regular exec output unused and connect Completed to Set bBusy (false).

Reaching Completed means that request's wait is over. But that alone does not guarantee you can use the intended mesh. Inspect the result before swapping.
On success, hand it to the display component
Add Cast to Static Mesh . Connect Async Load Asset's Object to the Cast's Object and Set bBusy's exec output to the Cast's exec input.
The Cast here asks "can what I received be used as a Static Mesh?". It does not convert to a different mesh, and this node does not load an asset itself. An empty result goes to Cast Failed.

Call Set Static Mesh from the Cast's success exec output. Drag DisplayMesh from Components into the graph and connect that reference to Target. Connect the Cast's As Static Mesh to New Mesh.

Target is "the component whose appearance changes" and New Mesh is "the asset it displays". Both are blue wires, but they name different things.
After Set Static Mesh, connect Set bApplied (true) and then a Print String ( Ready ). DisplayMesh keeps using the loaded asset, so the reference needed for display is held here.
On failure, keep the box
Connect Cast Failed to a Print String showing Load failed . Neither Set Static Mesh nor Set bApplied runs. The original box remains and both bBusy and bApplied are false.
That way the player can leave the range and re-enter to retry the load. With the asset left unset, though, retrying still fails. You have to fix the cause.
The whole flow looks like this. The split diagrams are all continuations of the same Event Graph.
RequestDisplayMesh (Custom Event)
→ Branch (bBusy OR bApplied)
True → do nothing
False → Set bBusy (true)
→ Print String (Loading...)
→ Async Load Asset (Asset = TargetMesh)
regular exec output → unconnected
Completed → Set bBusy (false)
→ Cast to Static Mesh (Object = the load's Object)
success → Set Static Mesh
Target = DisplayMesh
New Mesh = As Static Mesh
→ Set bApplied (true)
→ Print String (Ready)
Cast Failed → Print String (Load failed)
Confirm success and failure
Compile and save the Blueprint and place one BP_AsyncDisplay in the level. Make sure the box is visible on the floor and Player Start is outside LoadZone. About 500 cm away is comfortable for testing.
- Play and confirm the box is visible first.
- Approach the box. Seeing
Loading...thenReady, with the box becoming a cone, means success. - Leave the range and enter again. Since it is applied, confirm it does not repeat Loading...
- Stop Play, set TargetMesh's default to
None, and Compile. Play again and approach, and confirmLoad failedappears with the box remaining. - Leave and re-enter to confirm you can request again after a failure. Finally set TargetMesh back to SM_AsyncDemo and save.
With a small cone or an already loaded asset, the swap happens with almost no wait. That is fine. Seeing the swap confirms you can update the display from Completed. Keep that separate from proving it was unloaded or that memory was saved.

| Symptom | Where to check |
|---|---|
| It is already a cone from the start | DisplayMesh's initial mesh. A hard reference alone does not change appearance |
| Loading... never appears | LoadZone's Overlap settings, the player's Generate Overlap Events, Player Start being outside |
| It stops after Loading... | Whether Completed connects to Set bBusy |
| It reports Load failed | TargetMesh's default, whether the asset exists, whether it is included in the package |
| Ready appears but nothing changes | Whether Set Static Mesh's Target is DisplayMesh, New Mesh is the Cast result, Mobility is Movable |
| I cannot retry failure after the first time | Whether you are testing in a fresh Play. After success, bApplied stops requests by design |
For how to investigate Overlap see the collision article, and for reading logs see the Print String and Output Log article.
Check whether you really load less
From here we cover verification when applying this to a finished game. With just the small cone, a tiny difference is natural. We separate the tools by the question they answer.

Investigate the reference chain
Right-click BP_AsyncDisplay and open Reference Viewer . It traces what an asset references and what references it. Toggle Hard References and Soft References display and check the relationship to SM_AsyncDemo.
Here, TargetMesh should be a soft reference and DisplayMesh's default a hard reference to Cube. If SM_AsyncDemo is also hard-referenced by another level or Blueprint, a loading path remains from there.
Size Map in the same right-click menu compares the size of the selected asset and its dependencies as a diagram. It helps you inspect candidate sizes, but it does not display in-game memory at this instant. Read it while checking the display scope and reference kinds too.
Measure runtime memory separately
Opening a mesh in the editor or previewing it can already load it. To investigate the runtime effect, package a Development build and relaunch it for comparison, avoiding editor-side references.
- Build and launch a Development version containing your test level.
- Run
Memreport -fullin the console before entering the display's range. - Enter the range, confirm Ready, and run the same command again.
- Open the reports saved to the game's
Saved/Profiling/Memreportsand compare the breakdown and totals for the mesh and related textures. The save location varies by runtime environment.
Generating the report has its own cost, so do not confuse hitching during it with load performance. And do not attribute every change in total memory to SM_AsyncDemo; inspect the target's breakdown and other references.
If hitching during loading concerns you, record that period's work with Unreal Insights. Reference chains, memory used, and frame cost each examine different things.
Load shortly before use, and release when done
For clarity, the hands-on requested the load after approaching the display. When building it into a game, starting the load a little before you show it hides the wait.

For example, request in the corridor before the boss room so preparation progresses on the way. For a catalog, you might preload the few models most likely to be selected next. Preloading every candidate raises what you hold again, so decide the scope from both wait time and memory.
Conversely, things used from the start, like the player's default mesh and basic UI, are simpler prepared as hard references. Do not aim for "make everything soft"; consider assets whose appearance can be delayed.
What releasing a reference means
The loaded mesh is held while DisplayMesh uses it. When it is no longer needed, end that use by returning the display to another mesh or destroying the display piece.

If other variables or other display pieces use the same asset, it still remains. Once no unnecessary strong references remain, it becomes eligible for collection by a later garbage collection (GC) . GC is the mechanism that cleans up what is no longer used. Clearing a variable does not necessarily reduce memory usage by the same amount at that instant.
A soft reference is not what keeps an asset alive for continued use. Pass it to a display component as we did, or store the result in a regular Object Reference variable if you use it later.
Bonus: Good to Know Up Front
Getting a reference and loading are different. An operation that only finds an already loaded target returns empty when it is unloaded. Do not assume every conversion from a soft reference "loads synchronously automatically"; check which node actually loads. Where you want to avoid waiting during play, receive completion with Async Load Asset instead of Load Asset Blocking.
When extending to Skeletal Meshes, check the animation too. Changing a moving enemy's appearance requires the Skeleton and Animation Blueprint to match, not just the mesh. This article uses a Static Mesh to focus on the loading flow.
In UI that switches screens, watch the arrival order. Select item B right after requesting item A and A's result can arrive later. Before applying, confirm "do I still want to display this item" so an old result does not overwrite a new selection. Our example is fixed to one kind, so it omits that management.
Once asset counts grow, consider Asset Manager. It manages loading groups and holding and releasing. The Primary Data Asset introduced in the Data Asset article is a foundation for it, but creating one does not make things async by itself. Loading one asset as we did is enough to start.
A soft reference is not a substitute for packaging settings. Even assets present in the project cannot load in a shipped build if they are not included in the package. Especially when building paths dynamically from strings, check what is included in the Cook (conversion into runtime data).
Loading whole levels is a different unit. Loading a mesh with Async Load Asset does not make an Open Level transition async. For loading areas progressively, move on to level streaming and World Partition.
Summary
A soft reference is the mechanism for remembering an asset's location and loading it when needed. In the hands-on, approaching requested Async Load Asset, and we checked the result at Completed before swapping the box for a cone.
Start with one asset and get the loading, success, and failure behavior consistent. Then investigate the chain with Reference Viewer and measure runtime memory and cost, and you can judge which assets are worth deferring.
If the cost is not asset loading but repeatedly creating and destroying the same Actor, consider object pooling as well.