Quick answer: Addressables uses reference counting. Each call to LoadAssetAsync or InstantiateAsync increments the reference count, and each call to Release decrements it. Memory is only freed when the count reaches zero. If you load an asset three times but only release it twice, the asset stays in memory.
Here is how to fix Unity addressables memory not releasing. You switched to Unity Addressables for asset management, expecting cleaner memory usage and faster load times. Instead, your game’s memory footprint keeps climbing with every scene transition. Assets you thought were released are still resident in memory, and on mobile devices the OS eventually kills your app. The profiler shows textures and meshes lingering long after their scenes are gone. The problem is almost always mismatched load and release calls — Addressables uses reference counting, and a single missed release is enough to keep an entire asset bundle pinned in memory.
The Symptom
You load assets with Addressables.LoadAssetAsync or instantiate prefabs with Addressables.InstantiateAsync. When you are done with them — after a scene transition, a menu close, or an object pool drain — you expect the memory to drop. But the Unity Memory Profiler shows the assets are still allocated. Total memory usage ratchets upward with each level load. On a mobile device with 3 GB of RAM, your game starts getting low-memory warnings by the third level.
You might have added Addressables.Release calls in your cleanup code. The puzzling part is that some assets release correctly while others do not. The inconsistency makes it hard to identify a single root cause. Sometimes the leak only appears when you load and unload the same scene multiple times in a row.
If you open the Addressables Event Viewer, you see reference counts on certain assets that never drop to zero. An asset loaded once shows a reference count of one after release — meaning something still holds a reference. Or worse, an asset loaded once shows a reference count of three, meaning it was loaded multiple times without your knowledge.
What Causes This
Addressables memory leaks come from a fundamental mismatch between how developers expect memory management to work and how Addressables actually implements it.
1. Unbalanced load and release calls. Every call to LoadAssetAsync increments a reference count on the underlying asset. Every call to Release decrements it. Memory is only freed when the count reaches zero. If a component loads an asset in Start() but the scene transitions before OnDestroy() runs (or OnDestroy() does not call release), the reference count stays at one permanently. This is the single most common cause of Addressables memory leaks.
2. Using Object.Destroy instead of Addressables.ReleaseInstance. When you create a GameObject with Addressables.InstantiateAsync, the system tracks that instance internally. If you destroy it with Object.Destroy() instead of Addressables.ReleaseInstance(), the GameObject is removed from the scene but the Addressables reference count on the source prefab is never decremented. The prefab and its entire asset bundle stay in memory.
3. Multiple loads of the same asset without tracking handles. If two systems both call LoadAssetAsync for the same asset independently, the reference count is two. If only one system releases it, the count stays at one and the asset persists. This is especially common with shared assets like materials, audio clips, and UI sprites that multiple components request.
4. Forgetting to release the operation handle itself. Some developers release the result (the loaded asset) but not the AsyncOperationHandle. The handle is what carries the reference count. You must call Addressables.Release(handle) on the handle, not on the asset directly.
The Fix
Step 1: Track and release every handle. The most reliable pattern is to store every handle you create and release it explicitly when done. Never fire-and-forget a load operation.
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
public class EnemySpawner : MonoBehaviour
{
private AsyncOperationHandle<GameObject> _prefabHandle;
private GameObject _spawnedEnemy;
public async void SpawnEnemy(string address)
{
// Load the prefab and store the handle
_prefabHandle = Addressables.LoadAssetAsync<GameObject>(address);
await _prefabHandle.Task;
if (_prefabHandle.Status == AsyncOperationStatus.Succeeded)
{
_spawnedEnemy = Instantiate(_prefabHandle.Result);
}
}
private void OnDestroy()
{
// Clean up the instantiated object
if (_spawnedEnemy != null)
{
Destroy(_spawnedEnemy);
}
// Release the Addressables handle to decrement ref count
if (_prefabHandle.IsValid())
{
Addressables.Release(_prefabHandle);
}
}
}
Note the pattern: the handle is stored as a field, and OnDestroy always releases it. The IsValid() check prevents double-release errors if the handle was never assigned or was already released.
When using InstantiateAsync instead of LoadAssetAsync followed by manual instantiation, the pattern changes slightly — you must use ReleaseInstance instead of Destroy:
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
public class PickupSpawner : MonoBehaviour
{
private AsyncOperationHandle<GameObject> _instanceHandle;
public async void SpawnPickup(string address, Vector3 position)
{
// InstantiateAsync creates the object AND tracks it internally
_instanceHandle = Addressables.InstantiateAsync(address, position,
Quaternion.identity);
await _instanceHandle.Task;
}
private void OnDestroy()
{
// Do NOT use Destroy() here. Use ReleaseInstance to properly
// decrement the reference count and free the asset bundle.
if (_instanceHandle.IsValid())
{
Addressables.ReleaseInstance(_instanceHandle);
}
}
}
Step 2: Use the Addressables Event Viewer to find existing leaks. The Event Viewer is the definitive tool for debugging Addressables memory issues. To enable it, first make sure your AddressableAssetSettings has Send Profiler Events checked. Then open Window > Asset Management > Addressables > Event Viewer.
// Enable profiler events programmatically if needed
using UnityEngine.AddressableAssets;
public static class AddressablesDebugConfig
{
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void EnableDiagnostics()
{
// Enable diagnostics so the Event Viewer receives data
Addressables.ResourceManager.InternalIdTransformFunc += (location) =>
{
// Log every asset load for debugging
Debug.Log($"[Addressables] Loading: {location.InternalId}");
return location.InternalId;
};
}
}
In Play mode, the Event Viewer shows a timeline of every load and release operation. Each asset displays its current reference count. Play through a full cycle of your game — load a level, play it, return to the menu, load a different level. After returning to the menu, every asset from the previous level should show a reference count of zero. Any asset still showing a count above zero is a leak.
The Event Viewer groups assets by their parent bundle. If an entire bundle stays loaded because a single asset within it has a non-zero reference count, you will see the bundle listed with all its assets. This is important because a single leaked texture can keep an entire bundle — potentially tens of megabytes — pinned in memory.
Step 3: Implement a centralized handle manager. Rather than tracking handles in every MonoBehaviour individually, create a central manager that guarantees cleanup.
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.SceneManagement;
public class AddressablesHandleManager : MonoBehaviour
{
public static AddressablesHandleManager Instance { get; private set; }
private readonly Dictionary<string, AsyncOperationHandle> _loadedAssets
= new();
private readonly List<AsyncOperationHandle<GameObject>> _instances
= new();
private void Awake()
{
if (Instance != null)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
SceneManager.sceneUnloaded += OnSceneUnloaded;
}
public AsyncOperationHandle<T> LoadAsset<T>(string address)
{
var handle = Addressables.LoadAssetAsync<T>(address);
_loadedAssets[address] = handle;
return handle;
}
public AsyncOperationHandle<GameObject> SpawnInstance(
string address, Vector3 pos, Quaternion rot)
{
var handle = Addressables.InstantiateAsync(address, pos, rot);
_instances.Add(handle);
return handle;
}
public void ReleaseAsset(string address)
{
if (_loadedAssets.TryGetValue(address, out var handle))
{
Addressables.Release(handle);
_loadedAssets.Remove(address);
}
}
private void OnSceneUnloaded(Scene scene)
{
// Release all instances from the unloaded scene
foreach (var handle in _instances)
{
if (handle.IsValid())
{
Addressables.ReleaseInstance(handle);
}
}
_instances.Clear();
// Release all loaded assets
foreach (var kvp in _loadedAssets)
{
if (kvp.Value.IsValid())
{
Addressables.Release(kvp.Value);
}
}
_loadedAssets.Clear();
Debug.Log($"[HandleManager] Released all assets for scene: {scene.name}");
}
private void OnDestroy()
{
SceneManager.sceneUnloaded -= OnSceneUnloaded;
}
}
This manager acts as a single point of truth for all Addressables operations. Every load goes through it, every handle is tracked, and scene unloads trigger a complete cleanup. The pattern eliminates the most common leak scenario: a MonoBehaviour that loads an asset but gets destroyed before it can release the handle.
Why This Works
Addressables memory management is built entirely on reference counting. Unlike Unity’s older Resources system where you could call Resources.UnloadUnusedAssets() and let the garbage collector figure it out, Addressables requires explicit balance between loads and releases. This design gives you precise control over memory but demands discipline.
Storing and releasing handles ensures the reference count always returns to zero. When the count hits zero, Addressables unloads the asset. If all assets in a bundle reach zero, the bundle itself is unloaded, freeing the bulk of the memory. A single non-zero reference in a bundle keeps the entire bundle resident.
Using ReleaseInstance instead of Destroy works because InstantiateAsync creates a tracking entry inside the Addressables system. ReleaseInstance both destroys the GameObject and removes that tracking entry. Destroy only does the first half, leaving an orphaned entry that permanently holds the reference count at one or higher.
Centralizing handle management eliminates the distributed tracking problem. When every component tracks its own handles, a single missed release in any component causes a leak. A central manager with scene-unload cleanup acts as a safety net — even if a component forgets to release, the manager catches it when the scene ends.
Verifying the Fix
After implementing these changes, verify the fix with a specific test pattern: load a heavy scene, return to the menu, load the scene again, return to the menu again. Check memory usage after each menu return. If memory is stable (not growing), your leaks are fixed. If it still grows, open the Event Viewer and look for assets with non-zero reference counts after the second menu return.
You can also add a runtime diagnostic that logs the total number of tracked handles:
// Add this to your debug UI or call via a debug console
public void LogHandleStatus()
{
int loaded = _loadedAssets.Count;
int instances = _instances.Count;
Debug.Log($"[HandleManager] Loaded assets: {loaded}, Instances: {instances}");
foreach (var kvp in _loadedAssets)
{
Debug.Log($" Asset: {kvp.Key}, Valid: {kvp.Value.IsValid()}");
}
}
Related Issues
If your memory issues are not caused by Addressables but by Unity’s built-in asset references, the problem may be duplicate assets loaded through both Resources and Addressables simultaneously. Mixing the two systems for the same assets creates separate copies in memory. If your game uses asset bundles directly (without the Addressables wrapper), the release API is different — you need AssetBundle.Unload(true) instead of Addressables.Release.