Quick answer: Unity additive scenes often fail to unload because persistent scripts still hold references to objects inside them. Clear all cross-scene references, properly await the AsyncOperation from SceneManager.UnloadSceneAsync, and call Resources.UnloadUnusedAssets() afterward to free native memory.
Additive scene loading is one of Unity’s most powerful features for managing large game worlds. You can stream in dungeon rooms, UI overlays, or chunks of an open world without ever hitting a loading screen. But there’s a catch that bites nearly every Unity developer at least once: the scene you thought you unloaded is still lurking in memory, quietly consuming RAM until your game stutters, crashes, or your console logs fill with warnings about duplicate objects.
Why Additive Scenes Get Stuck
When you call SceneManager.UnloadSceneAsync, Unity marks every root GameObject in the target scene for destruction. The engine walks the hierarchy, calls OnDestroy on each component, and removes the scene from the loaded scenes list. In theory, that’s clean and simple.
In practice, the unload can silently fail or only partially succeed for several reasons:
- Cross-scene references. A manager script in your persistent scene holds a reference to a transform, renderer, or scriptable object that lives in the additive scene. Unity cannot destroy that object while something still points to it.
- Event subscriptions. The additive scene’s scripts subscribed to static events or delegates but never unsubscribed. Even after
OnDestroy, the delegate invocation list retains a reference to the destroyed component’s delegate target. - Coroutines on persistent objects. If a coroutine running on a persistent MonoBehaviour yields a reference to something in the additive scene (e.g., waiting for an animation to finish), that reference keeps the object alive across the GC boundary.
- Discarded AsyncOperation. Calling
UnloadSceneAsyncwithout storing or awaiting the returnedAsyncOperationcan lead to timing issues where you try to reload the scene before the unload finishes.
The Core Fix: Clean References Before Unloading
The single most important step is to sever every reference that crosses the scene boundary before you trigger the unload. Here is a pattern that works reliably in production:
public class SceneLoader : MonoBehaviour
{
private Scene _loadedLevel;
private List<System.Action> _cleanupActions = new();
public void RegisterCleanup(System.Action action)
{
_cleanupActions.Add(action);
}
public async Task UnloadCurrentLevel()
{
// Step 1: Run all registered cleanup callbacks
foreach (var action in _cleanupActions)
action?.Invoke();
_cleanupActions.Clear();
// Step 2: Unload and AWAIT completion
if (_loadedLevel.IsValid() && _loadedLevel.isLoaded)
{
AsyncOperation op = SceneManager.UnloadSceneAsync(_loadedLevel);
while (!op.isDone)
await Task.Yield();
}
// Step 3: Free native assets
await Resources.UnloadUnusedAssets();
GC.Collect();
}
}
Any system that grabs a reference to something in the additive scene registers a cleanup action. For example, your camera controller might do:
void TrackTarget(Transform target)
{
_followTarget = target;
FindObjectOfType<SceneLoader>().RegisterCleanup(() => _followTarget = null);
}
The AsyncOperation Trap
A surprisingly common mistake is to fire-and-forget the unload call:
// BAD: No await, no callback, no completion check
SceneManager.UnloadSceneAsync("Dungeon_03");
SceneManager.LoadSceneAsync("Dungeon_04", LoadSceneMode.Additive);
This creates a race condition. If Dungeon_04 shares any asset names or object names with Dungeon_03, Unity may get confused about which scene owns what. Even if the names don’t collide, loading before the unload finishes means both scenes are briefly in memory at the same time, doubling your peak memory usage.
Always await the operation. If you prefer coroutines over async/await, yield on the AsyncOperation directly:
IEnumerator SwapScenes(string oldScene, string newScene)
{
yield return SceneManager.UnloadSceneAsync(oldScene);
yield return Resources.UnloadUnusedAssets();
yield return SceneManager.LoadSceneAsync(newScene, LoadSceneMode.Additive);
}
Resources.UnloadUnusedAssets — the Forgotten Step
SceneManager.UnloadSceneAsync destroys GameObjects, but it does not free the underlying native assets. Textures, meshes, audio clips, and materials that were loaded as part of that scene remain in memory until Unity determines they are no longer referenced.
That determination only happens when you call Resources.UnloadUnusedAssets(). Without it, your memory usage graph will look like a staircase — each scene load adds memory, but no scene unload reclaims it.
A few things to keep in mind about UnloadUnusedAssets:
- It is an async operation itself. It scans the entire object graph, which can take 50–200ms depending on project size. Do not call it every frame.
- It only frees assets with zero references. This is why clearing cross-scene references first is essential — a single stray reference to a material will keep that material and its textures in memory.
- Pairing it with
GC.Collect()before the call ensures that managed wrappers around native objects are collected first, allowingUnloadUnusedAssetsto see them as truly unreferenced.
Detecting Leaks with the Memory Profiler
You suspect a leak but aren’t sure what’s holding on. The Unity Memory Profiler package (available via Package Manager) is the best tool for this:
- Take a memory snapshot after loading the additive scene.
- Unload the scene using your normal game flow.
- Take a second snapshot.
- Use the “Compare Snapshots” view to see what objects still exist in snapshot 2 that came from the unloaded scene.
Pay special attention to objects whose “Referenced By” chain leads back to a static field or a MonoBehaviour on a DontDestroyOnLoad object. Those are your leak sources. The Profiler window’s simple memory timeline is also useful — after a clean unload cycle, total memory should return roughly to baseline. If it ratchets up with each load/unload cycle, you have a leak.
Static Fields and DontDestroyOnLoad Pitfalls
Static fields are the most insidious source of scene memory leaks because they survive scene transitions by design. A common pattern that causes trouble:
public static class GameEvents
{
public static Action<Enemy> OnEnemyDefeated;
}
// In the additive scene's enemy script:
void OnEnable()
{
GameEvents.OnEnemyDefeated += HandleDefeat;
}
// If you forget OnDisable, this reference lives forever
void OnDisable()
{
GameEvents.OnEnemyDefeated -= HandleDefeat;
}
Every subscription to a static event from a scene object must have a corresponding unsubscription. The safest approach is to always pair OnEnable with OnDisable (not OnDestroy, because OnDestroy is not guaranteed to fire in the same frame as the unload). For DontDestroyOnLoad managers, audit every field that could point to a scene object and null it during the unload sequence.
“The memory leak wasn’t in the scene itself — it was a single static event handler in our audio manager that kept every enemy prefab alive. One missing unsubscribe cost us 400MB.”
Related Issues
If you’re working with Unity memory management, these posts may also help: Fix Unity Object Pooling Returning Wrong State covers another common source of unexpected memory behavior, and Best Practices for Error Logging in Game Code explains how to log memory warnings so you catch leaks before your players do.
Tip: Add a debug overlay that shows loaded scene count and total memory during development. If the scene count ever exceeds what you expect, you’ll catch the leak immediately instead of chasing it through profiler snapshots later.