Memory Is the Silent Killer
Frame rate gets all the attention, but memory is what silently kills UE5 projects. You won't notice the problem during development — your 64GB workstation handles everything fine. Then players on 16GB machines crash to desktop, and you're scrambling to figure out what went wrong.
Memory optimization isn't glamorous, but it's essential for shipping on platforms with tight memory budgets: consoles (unified memory), Steam Deck (16GB shared), and minimum-spec PCs.
Understanding UE5 Memory Usage
Where Memory Goes
A typical UE5 game's memory breakdown:
| Category | Typical Usage | Notes |
|---|---|---|
| Textures | 2-8 GB | Largest consumer, streamable |
| Meshes (Non-Nanite) | 0.5-2 GB | LOD levels, collision |
| Nanite Data | 0.5-3 GB | Virtualized geometry |
| Audio | 0.2-1 GB | Decompressed audio in memory |
| Blueprints/Code | 0.5-1 GB | Compiled Blueprint bytecode |
| Physics | 0.1-0.5 GB | Collision data, physics state |
| Navigation | 0.1-0.5 GB | NavMesh data |
| Particles | 0.1-0.5 GB | Niagara system data |
| Engine Overhead | 1-2 GB | UObject system, GC, framework |
Monitoring Memory
stat memory // High-level memory overview
stat memoryplatform // Platform-specific breakdown
memreport -full // Detailed report to log file
stat streaming // Texture/mesh streaming status
obj list // All loaded UObjects with sizes
For detailed analysis, use Unreal Insights with the memory trace channel:
UnrealEditor.exe YourProject.uproject -trace=memory,default
Hard References vs Soft References
This is the single most impactful memory concept in UE5.
Hard References
A hard reference loads the referenced asset when the referencing object loads:
// Hard reference — BP_Enemy loads when THIS Blueprint loads
UPROPERTY(EditDefaultsOnly)
TSubclassOf<AActor> EnemyClass;
// Hard reference — texture loads with this material
UPROPERTY(EditDefaultsOnly)
UTexture2D* DiffuseTexture;
If your main menu Blueprint hard-references your level's boss enemy, which hard-references its weapon, which hard-references explosion particles, which hard-reference destruction meshes... loading the main menu loads all of that into memory.
This is called reference chain bloat, and it's the #1 memory problem in UE5 projects.
Soft References
A soft reference stores the asset path without loading it:
// Soft reference — does NOT load automatically
UPROPERTY(EditDefaultsOnly)
TSoftClassPtr<AActor> EnemyClass;
UPROPERTY(EditDefaultsOnly)
TSoftObjectPtr<UTexture2D> DiffuseTexture;
Load soft references when you actually need them:
void AEnemySpawner::SpawnEnemy()
{
if (EnemyClass.IsNull()) return;
// Async load — doesn't block the game thread
FStreamableManager& Streamable = UAssetManager::GetStreamableManager();
Streamable.RequestAsyncLoad(EnemyClass.ToSoftObjectPath(),
FStreamableDelegate::CreateUObject(this, &AEnemySpawner::OnEnemyClassLoaded));
}
void AEnemySpawner::OnEnemyClassLoaded()
{
UClass* LoadedClass = EnemyClass.Get();
if (LoadedClass)
{
GetWorld()->SpawnActor<AActor>(LoadedClass, SpawnTransform);
}
}
The Rule
Use hard references for assets that are always needed (base character mesh, core UI textures, frequently used materials).
Use soft references for everything else — enemies, weapons, level-specific assets, optional content, cosmetics.
Finding Reference Chain Problems
Use the Reference Viewer (right-click any asset → Reference Viewer) to visualize dependency chains. Look for:
- Assets referenced from many places (potential for unnecessary loading)
- Long chains that pull in unrelated content
- Large assets (textures, meshes) referenced by small objects (data tables, configs)
The Size Map tool (Window → Developer Tools → Size Map) shows the total memory footprint of an asset including all its dependencies.
Texture Streaming
How It Works
UE5 streams textures at runtime, loading only the mip levels needed for the current view:
- A texture 50 meters away loads a low-resolution mip (512x512)
- As the camera approaches, higher mips stream in (1024, 2048, 4096)
- Distant textures drop to lower mips to free memory
Streaming Pool Configuration
The Texture Streaming Pool is a fixed-size memory budget for streamed textures:
; DefaultEngine.ini
[/Script/Engine.RendererSettings]
r.Streaming.PoolSize=1000 ; Pool size in MB
Sizing the pool:
- Minimum spec PC (8GB RAM): 512-768 MB
- Mid-range PC (16GB): 1000-1500 MB
- High-end PC / Console: 1500-2500 MB
- Steam Deck: 512-768 MB
If the pool is too small, textures visibly pop in as blurry-to-sharp. If too large, it steals memory from other systems.
Texture Optimization
- Max texture size: Most textures don't need 4096x4096. Set per-texture max size in the texture editor
- Compression: Use BC7 for quality, BC1/BC3 for size-sensitive textures
- Virtual Textures: For landscape and large-surface materials, virtual textures stream tiles instead of mips
- Texture groups: Assign textures to groups (World, Character, UI, Effects) with per-group streaming settings
- Never Stream flag: Only set for textures that must be fully loaded (UI, critical effects)
World Partition
For open-world games, World Partition is essential for memory management.
How It Works
World Partition divides your level into a grid of cells. Only cells near the player are loaded — everything else is streamed out.
Setting Up World Partition
- In Level Settings, enable World Partition
- Set Cell Size: 12800-25600 units is typical (128-256 meters)
- Set Loading Range: How far from the player cells load (usually 2-3x cell size)
- Place actors — they're automatically assigned to cells based on position
Streaming Configuration
; Per-actor streaming distance override
bOverrideWorldPartitionLoadingRange=True
LoadingRange=50000 ; Override for specific actors like landmarks
Loading range by actor type:
- Terrain/Landscape: Match view distance (50,000+ units)
- Large buildings: 30,000-50,000 units
- Trees/Foliage: 15,000-30,000 units
- Small props: 5,000-15,000 units
- Ground clutter: 3,000-5,000 units
HLOD (Hierarchical Level of Detail)
For cells that are loaded but distant, HLOD provides simplified representations:
- HLOD0: Simplified meshes for medium distance
- HLOD1: Even simpler (merged) meshes for far distance
- HLOD2: Impostor/billboard for extreme distance
Configure HLOD layers in World Partition settings. Build HLODs before shipping.
Asset Manager and Primary Asset Types
For large projects, the Asset Manager provides fine-grained control over asset loading:
// Register primary asset types in DefaultGame.ini
[/Script/Engine.AssetManagerSettings]
+PrimaryAssetTypesToScan=(PrimaryAssetType="Map", AssetBaseClass=/Script/Engine.World, ...)
+PrimaryAssetTypesToScan=(PrimaryAssetType="EnemyData", AssetBaseClass=/Script/MyGame.UEnemyDataAsset, ...)
Then load asset bundles on demand:
// Load all assets for "Level_Forest" bundle
FPrimaryAssetId LevelAssetId = FPrimaryAssetId("Map", "Level_Forest");
UAssetManager::Get().LoadPrimaryAsset(LevelAssetId,
{"Visual", "Gameplay"},
FStreamableDelegate::CreateLambda([this]() {
// Assets loaded, safe to use
}));
Garbage Collection
UE5's garbage collector runs periodically to clean up unreferenced UObjects. Understanding GC helps avoid memory leaks:
Common Leak Patterns
Leaked references: Holding a UPROPERTY pointer to an object prevents GC from collecting it, even after the object is logically "destroyed."
// This leaks if the spawned actor is destroyed but SpawnedEnemy still holds a reference
UPROPERTY()
AActor* SpawnedEnemy;
// Fix: Null the pointer when the actor is destroyed
SpawnedEnemy->OnDestroyed.AddDynamic(this, &ASpawner::OnEnemyDestroyed);
void ASpawner::OnEnemyDestroyed(AActor* Actor) { SpawnedEnemy = nullptr; }
TArray of UObject pointers: Growing arrays that never shrink hold references indefinitely. Clear arrays when done.
Delegates: Bound delegates prevent GC of the bound object. Always unbind in EndPlay/BeginDestroy.
GC Tuning
; Increase GC frequency for memory-constrained platforms
gc.TimeBetweenPurgingPendingKillObjects=30 ; Seconds between GC runs (default: 60)
gc.MaxObjectsNotConsideredByGC=1 ; Force GC to check all objects
Memory Budget Planning
Before building content, set memory budgets:
| Category | Budget (16GB target) |
|---|---|
| OS + Drivers | 2-3 GB |
| Engine Overhead | 1.5 GB |
| Texture Streaming Pool | 1 GB |
| Meshes + Nanite | 1.5 GB |
| Audio | 0.5 GB |
| Gameplay Objects | 1 GB |
| Physics + Navigation | 0.5 GB |
| Headroom | 1-2 GB |
| Total Available | ~10-12 GB |
Track actual usage against budgets weekly. Catching memory creep early is infinitely easier than fixing it before launch.
Quick Wins
- Audit hard references: Use the Reference Viewer on your most-loaded Blueprints. Convert unnecessary hard refs to soft refs.
- Set texture max sizes: Most environment textures don't need 4096. Set to 2048 or 1024 where quality allows.
- Enable texture streaming: Ensure
r.Streaming.PoolSizeis set appropriately and textures aren't marked "Never Stream" unnecessarily. - Use World Partition: If your game has levels larger than 500m, World Partition saves memory automatically.
- Profile on target hardware: Your dev machine hides memory problems. Test on minimum spec early and often.
Memory optimization is ongoing work, not a one-time task. Build awareness of memory usage into your development habits, and your game will run smoothly on the hardware your players actually own.