UE5 memory keeps increasing: how to find memory leaks in Unreal
You run a play session, open Task Manager after twenty minutes and the game is using far more RAM than when it started. Load a level, go back to the menu, load it again, and the number climbs a little more each time. Eventually a console build runs out of memory, or a long PC session starts hitching. This is one of the most common performance questions in Unreal Engine 5, and the frustrating part is that it is often not a real leak at all.
This guide walks through how to tell the difference, how to find what is actually holding memory with tools that ship with the engine, and which coding patterns cause most UE5 leaks.
Why memory grows in UE5 (and why it is not always a leak)
Unreal does not hand memory back to the OS the moment you stop using it. Several systems hold memory on purpose:
- Garbage collection runs periodically, not instantly. A UObject you stopped referencing still exists until the next collection pass.
- The allocator keeps freed memory cached for reuse, so the process size in Task Manager can stay high after objects are destroyed.
- Texture streaming fills its pool up to the configured budget. Seeing that budget used is normal.
- Editor (PIE) sessions keep extra data alive: asset editors, undo history, cached derived data. Numbers measured in the editor are not representative of a packaged game.
A real leak has one clear signature: memory goes up across repeated identical cycles and never comes back down. If loading the same level five times leaves you higher after each round trip, you have something to fix. If it rises once and then plateaus, you are usually looking at caches doing their job.
Step 1: Build a repeatable test
Do not hunt leaks by playing randomly. Pick a loop that you can repeat exactly: open a map, play for a minute, return to the main menu. Run it in a packaged Development build, not in PIE. Record memory at the same point of every cycle (for example, in the main menu after the level has fully unloaded). Five cycles is usually enough to see whether the trend is flat or climbing.
Step 2: Get numbers from inside the engine
The console gives you a quick first look:
stat memory
stat streaming
obj list
memreport -full
stat memory and stat streaming show live overlays. obj list prints UObject counts per class, which is the fastest way to spot a leak in gameplay code: if the count for your projectile or widget class keeps growing between cycles, you have found your suspect. You can narrow it with obj list class=YourClassName.
memreport -full writes a detailed report into your project's Saved/Profiling/MemReports folder. Take one report per cycle and compare them. The class with a rising object count or size is where you start reading code.
For more detail, launch the game with the Low Level Memory Tracker enabled (-llm) and use stat llm or stat llmfull. LLM groups memory by category (textures, meshes, audio, UObjects and so on), so you can see which subsystem is growing instead of just a single total.
Step 3: Use Unreal Insights for allocation call stacks
When you know that memory grows but not who allocates it, use Unreal Insights with memory tracing. Start the packaged game with a trace channel for memory, for example -trace=default,memory, connect Unreal Insights, run your test loop, and open the trace. The Memory Insights view lets you look at allocations that were made in a time range and never freed, grouped by call stack. That turns "RAM goes up" into a specific function in your code.
Keep traces short and focused on one or two cycles. Full allocation tracing produces large files and slows the game down, which is fine for diagnosis but not for timing measurements.
Step 4: Check the usual suspects in your code
In practice, most UE5 leaks come from a short list of patterns:
- Objects you keep referencing. A UObject stored in a
UPROPERTY()array, a global manager or a subsystem will never be garbage collected. Arrays that only getAddand neverRemoveare the classic case, for example a list of spawned actors that is never cleaned up when they are destroyed. - AddToRoot without RemoveFromRoot. Rooted objects are excluded from garbage collection until you un-root them.
- Strong references held outside the reflection system, such as
TStrongObjectPtror objects reported throughFGCObject::AddReferencedObjects, that outlive the thing that needed them. - Delegates and timers bound to objects that should have died. Unbind in
EndPlayorDeinitialize, and clear timers you started. - Plain C++ allocations:
newwithout a matchingdelete, orTSharedPtrcycles where two objects hold shared pointers to each other. UseTWeakPtrto break one side of the cycle. - Widgets created every time a menu opens and only removed from the viewport, while a reference to the old widget is still stored somewhere.
To find out why a specific object is still alive, the obj refs console command can print the reference chain that keeps it from being collected. This is often faster than reading code, because it points you straight at the property or container holding it.
Step 5: Verify the fix the same way you found the bug
After a change, run the exact same test loop and compare. The goal is a flat line across cycles: memory may rise on the first load, but every later cycle should return to roughly the same value. Keep the old and new reports side by side; a fix that looks right in code but does not change the numbers did not fix the leak.
What not to do
- Do not judge by Task Manager alone. Process size includes allocator caches and does not tell you what is leaking.
- Do not profile only in the editor. PIE keeps extra objects alive and hides or invents problems.
- Do not force garbage collection every frame to make the number look better. It hides the leak and costs frame time.
- Do not shrink the texture streaming pool to "fix" memory growth before you know textures are the cause.
- Do not use raw UObject pointers in long-lived containers without a
UPROPERTY()or a weak pointer. The garbage collector does not see them, which can give you dangling pointers instead of leaks.
Summary
Memory that grows in UE5 is only a leak if it keeps growing across identical, repeatable cycles. Build a fixed test loop in a packaged build, use obj list and memreport -full to find the class that grows, LLM to find the subsystem, and Unreal Insights memory tracing to find the call stack. Most fixes come down to removing a reference you forgot about: a container, a rooted object, a delegate or a widget. Then prove the fix by running the same loop again and getting a flat line.
Disclosure: I build and sell this plugin.