[UE5] Soft References and Async Loading: Load the Mesh Only Once You Need It

Created: 2026-07-20Last updated: 2026-09-07

Sorts out hard and soft references by when loading happens. Builds a Blueprint where a box becomes a cone as you approach, and diagrams Async Load Asset's completion, failure, and retry, plus how to inspect references and runtime memory.

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.

Remembering an asset's location and loading it when needed, turning the box into a cone

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

Sponsored

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.

Hard references load dependencies while soft references record the asset's location

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 comparisonHard referenceSoft reference
Loading the side that stores itLoads the target as wellDoes not load the target from that reference alone
Using the assetNames an already loaded targetRequests a load and uses the result you receive
When to choose itUsed from the start, must not waitUsed 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 laterReference kindLoading node
Assets like meshes, images, soundsSoft Object ReferenceAsync Load Asset
Blueprint classes such as Actors to SpawnSoft Class ReferenceAsync 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.

Synchronous loading waits for completion; asynchronous loading receives Completed later
Async Load Asset pinRole
AssetThe soft reference you want loaded
The upper regular exec output (Out)Work that continues without waiting. Unused in this hands-on
CompletedContinues to work that uses the load result
ObjectReceives 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.

The finished display: a box before you approach, a cone after a successful load

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.

NameComponentSettings
DisplayMeshStatic MeshMesh = /Engine/BasicShapes/Cube , Location = (0, 0, 50) , Mobility = Movable , Collision Presets = NoCollision
LoadZoneBox CollisionLocation = (0, 0, 100) , Box Extent = (200, 200, 150) , Collision Presets = OverlapOnlyPawn , Generate Overlap Events on
BP_AsyncDisplay's component hierarchy and LoadZone's size

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.

Choosing Static Mesh Soft Object Reference and setting SM_AsyncDemo as the default
Variable nameTypeDefaultMeaning
TargetMeshStatic Mesh Soft Object ReferenceSM_AsyncDemoWhere the mesh to load later lives
bBusyBooleanfalseA load is currently pending
bAppliedBooleanfalseThe 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.

Sponsored

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.

Comparing Other Actor with Get Player Character's result using Equal Object

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.

Going from Overlap to a Branch and calling RequestDisplayMesh on a match

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.

Checking whether either bBusy or bApplied is true with OR Boolean

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... .

From the Branch's False, setting bBusy to true and printing 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).

Passing TargetMesh to Async Load Asset and setting bBusy false on Completed

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.

Checking the load result with Cast to Static Mesh and printing Load failed on failure

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.

Wiring DisplayMesh's Target and the load result's New Mesh separately, then marking it applied

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.

  1. Play and confirm the box is visible first.
  2. Approach the box. Seeing Loading... then Ready , with the box becoming a cone, means success.
  3. Leave the range and enter again. Since it is applied, confirm it does not repeat Loading...
  4. Stop Play, set TargetMesh's default to None , and Compile. Play again and approach, and confirm Load failed appears with the box remaining.
  5. 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.

On success, Loading... is followed by the box becoming a cone; on failure, Load failed appears and the box remains
SymptomWhere to check
It is already a cone from the startDisplayMesh's initial mesh. A hard reference alone does not change appearance
Loading... never appearsLoadZone'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 failedTargetMesh's default, whether the asset exists, whether it is included in the package
Ready appears but nothing changesWhether Set Static Mesh's Target is DisplayMesh, New Mesh is the Cast result, Mobility is Movable
I cannot retry failure after the first timeWhether 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.

Sponsored

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.

Investigating reference relationships, dependency size, and runtime memory with separate tools

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.

  1. Build and launch a Development version containing your test level.
  2. Run Memreport -full in the console before entering the display's range.
  3. Enter the range, confirm Ready, and run the same command again.
  4. Open the reports saved to the game's Saved/Profiling/Memreports and 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.

Requesting the load in the corridor shortly before reaching the display

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.

Even when the display component releases its reference, another display piece using it keeps it alive

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.

Unreal Engine Notes in this section98