Unity Memory Leaks - Performance Optimization (How to Fix)
Problem: Your Unity game or editor slows down over time, uses more and more RAM, or crashes after long Play Mode sessions. Frames drop, the Profiler shows climbing memory, and restarting is the only relief.
Quick solution: Use the Profiler to find what is allocating memory and not being released, then fix the most common causes: missing cleanup in scripts, unsubscribed events, and unnecessary allocations in Update or other hot paths.
Why Unity Memory Leaks Happen
In Unity, a "memory leak" usually means one of these:
- Managed heap growth – C# objects (and arrays, strings, delegates) are allocated but never become eligible for garbage collection because something still holds a reference.
- Native/resource leaks – Unity engine objects (textures, meshes, audio clips) or native plugins are created and not released.
- Accumulation in Play Mode – Objects are instantiated or allocated every frame or every time a scene loads, and never destroyed or pooled.
Common causes:
- Instantiating objects without calling
Destroyor using a pool - Subscribing to events (UnityEvent, C# events, delegates) and never unsubscribing
- Holding references in static variables or long-lived objects so the GC cannot collect
- Creating new collections (List, Dictionary, array) or strings inside
Update()or other frequently called methods - Not releasing AssetBundle, Addressables, or other loaded assets when done
- Native objects (e.g. Texture2D, RenderTexture) created in code and not destroyed
Step 1: Find the Leak with the Profiler
Before changing code, confirm where memory is going.
- Open Window > Analysis > Profiler.
- Switch to the Memory module.
- Enter Play Mode and click Record.
- Play until you see the problem (e.g. after 1–2 minutes or after repeating an action).
- Take a Memory Snapshot (Deep Profile) or watch the Used Total and Reserved bars over time.
What to look for:
- Used Total or GC Alloc climbing every frame or every few seconds – often allocations in Update or in repeated logic.
- Simple or Detailed view showing which scripts or system allocations are growing.
- After stopping Play Mode, if memory does not drop back toward the initial level, something is still being held (e.g. static references, DontDestroyOnLoad objects, or engine resources).
Pro tip: Use Deep Profile for a more accurate script-level breakdown. It is slower but shows which of your scripts allocate the most.
Step 2: Fix Common Memory Leak Patterns
Pattern 1: Instantiate Without Destroy or Pool
Problem: Objects are spawned (e.g. bullets, effects, enemies) and never destroyed or reused.
Fix:
- Call
Destroy(gameObject)orDestroy(gameObject, delay)when the object is no longer needed. - For objects created often (e.g. bullets, VFX), use an object pool so you reuse instances instead of instantiating and destroying every time.
// BAD: New object every time, never destroyed
void Fire()
{
var bullet = Instantiate(bulletPrefab);
bullet.transform.position = firePoint.position;
}
// GOOD: Destroy when done or use a pool
void Fire()
{
var bullet = Instantiate(bulletPrefab);
bullet.transform.position = firePoint.position;
Destroy(bullet, 5f); // or return to pool after 5 seconds
}
Pattern 2: Event and Delegate Subscriptions Not Removed
Problem: You subscribe to events (OnEnable, UnityEvent, C# event, or delegate) but never unsubscribe. The subscriber holds a reference so the publisher (and anything it references) cannot be collected.
Fix:
- Unsubscribe in
OnDisable()orOnDestroy()for every subscription made inOnEnable()or elsewhere.
// BAD: Subscribe in OnEnable, never unsubscribe
void OnEnable()
{
someObject.OnSomething += HandleSomething;
}
// GOOD: Unsubscribe in OnDisable
void OnEnable()
{
someObject.OnSomething += HandleSomething;
}
void OnDisable()
{
if (someObject != null)
someObject.OnSomething -= HandleSomething;
}
Pattern 3: Allocations in Update or FixedUpdate
Problem: Every frame you create new objects (e.g. new List<>(), new Vector3(), or string operations) that add pressure on the garbage collector and can cause hitches.
Fix:
- Reuse collections and cache results. Use class-level or static lists and clear them instead of creating new ones each frame.
- Avoid creating strings or temporary objects in hot paths when possible.
// BAD: New list every frame
void Update()
{
var hits = new List<RaycastHit>();
Physics.RaycastAll(transform.position, Vector3.down, hits);
}
// GOOD: Reuse list
List<RaycastHit> hits = new List<RaycastHit>();
void Update()
{
hits.Clear();
Physics.RaycastAll(transform.position, Vector3.down, hits);
}
Pattern 4: Static or Long-Lived References
Problem: A static list or singleton holds references to objects (e.g. enemies, UI elements). When you think you have "removed" them, you only stopped using them in the scene; the static reference still keeps them alive.
Fix:
- Remove objects from static collections when they are destroyed or no longer needed.
- Avoid storing references to GameObjects or MonoBehaviours in static fields unless you explicitly manage their lifetime.
Pattern 5: Native Resources Not Released
Problem: You create textures, render textures, or other native resources in script and never call the appropriate destroy or release method.
Fix:
- For RenderTexture, Texture2D, or similar created at runtime, call
Destroy(tex)or the API’s release method when no longer needed. - Use
usingblocks or try/finally for disposable objects so they are always released.
Step 3: Verify the Fix
- Profiler again – Enter Play Mode, perform the same actions that used to increase memory, and run for the same duration. Memory (Used Total / GC Alloc) should stabilize instead of climbing.
- Exit Play Mode – After stopping Play Mode, take another snapshot. Memory should return closer to the pre-Play level; if it stays high, something still holds references (e.g. statics, DontDestroyOnLoad).
- Repeat the scenario – Load the same scene or repeat the same gameplay loop several times. If memory grows each time, look for allocations in scene load or in logic that runs per round.
Alternative Fixes and Edge Cases
- Addressables / AssetBundles – If you load assets at runtime, release them when no longer needed (e.g.
Addressables.ReleaseorAssetBundle.Unload). Otherwise references to loaded assets can keep memory high. - Coroutines – Coroutines that run for a long time or hold references to large objects can keep those objects alive. Stop coroutines when the behavior is no longer needed and avoid holding heavy references in local variables for the whole duration.
- Third-party plugins – Some plugins allocate native memory. Check their docs for cleanup or dispose APIs and call them when appropriate (e.g. when disabling the component or changing scenes).
Prevention Tips
- Use the Profiler regularly – Short sessions with the Memory module help catch leaks early.
- Code review – Check every
Instantiate, event subscription, and static collection; ensure there is a matching destroy, unsubscribe, or remove. - Object pooling – For frequently spawned objects (projectiles, effects, enemies), implement or use a pool and reuse instances.
- Minimize allocations in Update – Cache references, reuse lists, and avoid new allocations in hot paths.
Related Help and Guides
- Unity Freezes During Play Mode - How to Fix (Memory Issues) – When the editor hangs or becomes unresponsive.
- Unity Performance Drops to 10 FPS - How to Fix – General performance and frame rate issues.
- Unity Profiler Not Showing Data - Performance Analysis Fix – If the Profiler is not giving useful data.
- Unity Beginner Guide – Core Unity concepts and scripting.
Bookmark this page for quick reference when tracking down memory issues. If this fix helped, share it with other Unity devs who are fighting leaks or stutters.