Quick answer: Textures loaded via Resources.Load or created at runtime (new Texture2D) are not automatically unloaded when a scene changes. Unity only unloads assets that have zero references. If any script, material, or static variable still holds a reference to the texture, it stays in memory.

Here is how to fix Unity memory leak texture not releasing. Your Unity game's memory usage climbs steadily the longer you play. Each scene transition adds 50, 100, or 200 megabytes that never get freed. Eventually the game slows to a crawl, starts stuttering, and on mobile devices, gets killed by the OS. The culprit is almost always textures — they are the single largest consumers of GPU and CPU memory in most games, and Unity does not automatically clean them up the way many developers expect.

How Texture Memory Leaks Happen

Unity manages memory for assets in two very different ways depending on how you load them. Assets that are directly referenced in a scene (dragged onto a material, assigned in the Inspector) are loaded when the scene loads and unloaded when the scene is destroyed. This is the "managed" path and it generally works correctly.

The problems start when you load textures at runtime. This includes textures loaded via Resources.Load(), textures created with new Texture2D(), textures downloaded from the internet, render textures allocated with RenderTexture.GetTemporary(), and textures generated by screenshot or camera capture code. These runtime-created textures are not tied to any scene's lifecycle. They exist in memory until you explicitly destroy them or until Resources.UnloadUnusedAssets() determines they have zero references.

The subtlety is in what counts as a "reference." If a texture is assigned to a material, the material references it. If a material is assigned to a renderer, the renderer references the material, which references the texture. If the renderer is on a GameObject that exists in the scene, the entire chain is considered "in use." But static variables, singletons, cached lists, and event delegates can also hold references — and these persist across scene changes.

A single 2048x2048 RGBA32 texture uses 16MB of memory. Ten leaked textures of that size consume 160MB. On a mobile device with 2-3GB total RAM (and far less available to your app), this is enough to trigger an out-of-memory kill.

Identifying Leaks with the Memory Profiler

Before you can fix a leak, you need to know exactly which textures are leaking. Unity's Memory Profiler package provides the tools you need. Install it via the Package Manager (Window > Package Manager, search for "Memory Profiler") and open it from Window > Analysis > Memory Profiler.

The workflow for finding texture leaks is straightforward:

// Step 1: Add a debug helper to trigger memory snapshots
using UnityEngine;
using UnityEngine.Profiling;

public class MemoryDebug : MonoBehaviour
{
    void Update()
    {
        // Press M to log current memory stats
        if (Input.GetKeyDown(KeyCode.M))
        {
            long totalAllocated = Profiler.GetTotalAllocatedMemoryLong();
            long totalReserved = Profiler.GetTotalReservedMemoryLong();
            long gfxDriver = Profiler.GetAllocatedMemoryForGraphicsDriver();

            Debug.Log(string.Format(
                "[Memory] Allocated: {0:F1}MB | Reserved: {1:F1}MB | GFX: {2:F1}MB",
                totalAllocated / (1024f * 1024f),
                totalReserved / (1024f * 1024f),
                gfxDriver / (1024f * 1024f)));
        }

        // Press U to force unload unused assets
        if (Input.GetKeyDown(KeyCode.U))
        {
            Resources.UnloadUnusedAssets();
            System.GC.Collect();
            Debug.Log("[Memory] Forced unload and GC");
        }
    }
}

Take a Memory Profiler snapshot after the initial scene loads (this is your baseline). Then transition to another scene and back. Take a second snapshot. Compare the two snapshots — the Memory Profiler shows a diff view that highlights objects present in the second snapshot but not the first. Filter by "Texture2D" to see exactly which textures were added and never released.

Pay special attention to the "Referenced By" chain in the snapshot. It tells you exactly what is holding onto each leaked texture. Common culprits include static dictionaries used for texture caching, material property blocks that duplicated a material, and event handlers that captured a reference to a texture in a closure.

Destroying Runtime-Created Textures

The most direct fix for texture leaks is to explicitly destroy textures you create at runtime. Any texture allocated with new Texture2D() must be destroyed with Destroy() when you are done with it.

using UnityEngine;
using UnityEngine.Networking;
using System.Collections;

public class TextureDownloader : MonoBehaviour
{
    private Texture2D downloadedTexture;

    public IEnumerator DownloadTexture(string url)
    {
        // Destroy the previous texture before loading a new one
        if (downloadedTexture != null)
        {
            Destroy(downloadedTexture);
            downloadedTexture = null;
        }

        using (var request = UnityWebRequestTexture.GetTexture(url))
        {
            yield return request.SendWebRequest();

            if (request.result == UnityWebRequest.Result.Success)
            {
                // This creates a NEW Texture2D - you own it
                downloadedTexture = DownloadHandlerTexture.GetContent(request);
            }
        }
    }

    void OnDestroy()
    {
        // Clean up when this component is destroyed
        if (downloadedTexture != null)
        {
            Destroy(downloadedTexture);
        }
    }
}

A common mistake is assigning a runtime texture to a material's mainTexture and then never cleaning it up. When you set renderer.material.mainTexture = myTexture, Unity silently creates a copy of the material (a "material instance") to avoid modifying the shared material. Now you have both a leaked material instance and a leaked texture. You need to destroy both:

using UnityEngine;

public class DynamicTextureUser : MonoBehaviour
{
    private Material materialInstance;
    private Texture2D dynamicTexture;

    void Start()
    {
        // Create a dynamic texture
        dynamicTexture = new Texture2D(512, 512, TextureFormat.RGBA32, false);
        FillTexture(dynamicTexture);

        // Accessing .material creates an instance - cache it
        Renderer rend = GetComponent<Renderer>();
        materialInstance = rend.material; // This is now an instance
        materialInstance.mainTexture = dynamicTexture;
    }

    void OnDestroy()
    {
        // Destroy both the material instance and the texture
        if (materialInstance != null)
        {
            Destroy(materialInstance);
        }
        if (dynamicTexture != null)
        {
            Destroy(dynamicTexture);
        }
    }

    private void FillTexture(Texture2D tex)
    {
        Color[] pixels = new Color[tex.width * tex.height];
        for (int i = 0; i < pixels.Length; i++)
        {
            pixels[i] = Color.white;
        }
        tex.SetPixels(pixels);
        tex.Apply();
    }
}

Use renderer.sharedMaterial instead of renderer.material when you want to modify the shared material without creating an instance. But be careful — changes to sharedMaterial affect every object using that material.

Render Texture Management

Render textures are one of the most frequent sources of memory leaks because they are often created and used in places where cleanup is not obvious. Every camera that renders to a texture, every post-processing effect, and every minimap or UI element that uses a render texture is a potential leak.

using UnityEngine;

public class SafeRenderTexture : MonoBehaviour
{
    private RenderTexture renderTex;

    void CreateRenderTexture()
    {
        // Always release the old one before creating a new one
        ReleaseRenderTexture();

        renderTex = new RenderTexture(1024, 1024, 24);
        renderTex.name = "MyRT_" + gameObject.name;
        renderTex.Create();
    }

    void ReleaseRenderTexture()
    {
        if (renderTex != null)
        {
            renderTex.Release(); // Release GPU resources
            Destroy(renderTex);   // Destroy the Unity object
            renderTex = null;
        }
    }

    void OnDestroy()
    {
        ReleaseRenderTexture();
    }
}

For temporary render textures, use RenderTexture.GetTemporary() and RenderTexture.ReleaseTemporary(). Unity pools these internally, so they are more efficient than creating and destroying render textures repeatedly. But you must release them — a "temporary" render texture that is never released is still a memory leak:

using UnityEngine;

public class ScreenCapture : MonoBehaviour
{
    public Texture2D CaptureScreen()
    {
        // Get a temporary render texture from the pool
        RenderTexture temp = RenderTexture.GetTemporary(
            Screen.width, Screen.height, 0);

        // Render the camera to it
        Camera main = Camera.main;
        main.targetTexture = temp;
        main.Render();
        main.targetTexture = null;

        // Read pixels from the render texture
        RenderTexture previous = RenderTexture.active;
        RenderTexture.active = temp;

        Texture2D screenshot = new Texture2D(
            Screen.width, Screen.height, TextureFormat.RGB24, false);
        screenshot.ReadPixels(new Rect(0, 0, Screen.width, Screen.height), 0, 0);
        screenshot.Apply();

        // CRITICAL: Restore and release the temporary render texture
        RenderTexture.active = previous;
        RenderTexture.ReleaseTemporary(temp);

        // The caller owns the screenshot Texture2D and must Destroy it
        return screenshot;
    }
}

Note the pattern: the temporary render texture is released immediately, but the Texture2D screenshot is returned to the caller, who becomes responsible for destroying it. This ownership transfer must be documented clearly, or the calling code will leak the texture.

Sprite Atlas and Asset Bundle Leaks

Sprite atlases can cause subtle leaks. When you load a single sprite from an atlas, Unity loads the entire atlas texture into memory. If you then destroy the sprite but other sprites from the same atlas are still referenced, the atlas stays in memory. This is correct behavior, but it can be surprising if you expected the atlas memory to scale with how many sprites you are using.

using UnityEngine;
using UnityEngine.U2D;

public class SpriteAtlasManager : MonoBehaviour
{
    [SerializeField] private SpriteAtlas uiAtlas;

    // Track which sprites we've loaded so we can check references
    private System.Collections.Generic.Dictionary<string, Sprite> loadedSprites
        = new System.Collections.Generic.Dictionary<string, Sprite>();

    public Sprite GetSprite(string spriteName)
    {
        if (!loadedSprites.TryGetValue(spriteName, out Sprite sprite))
        {
            sprite = uiAtlas.GetSprite(spriteName);
            loadedSprites[spriteName] = sprite;
        }
        return sprite;
    }

    public void ClearCache()
    {
        // Clear our reference cache so the atlas can be unloaded
        loadedSprites.Clear();
    }
}

With the Addressables system, texture memory management is handled through reference counting. Each time you load an asset, the reference count increments. Each time you release it, the count decrements. When it reaches zero, the asset and its associated bundle are unloaded. The critical rule is: every Addressables.LoadAssetAsync must have a matching Addressables.Release:

using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;

public class AddressableTextureLoader : MonoBehaviour
{
    private AsyncOperationHandle<Texture2D> textureHandle;

    public void LoadTexture(string address)
    {
        // Release previous handle if it exists
        if (textureHandle.IsValid())
        {
            Addressables.Release(textureHandle);
        }

        textureHandle = Addressables.LoadAssetAsync<Texture2D>(address);
        textureHandle.Completed += OnTextureLoaded;
    }

    private void OnTextureLoaded(AsyncOperationHandle<Texture2D> handle)
    {
        if (handle.Status == AsyncOperationStatus.Succeeded)
        {
            Renderer rend = GetComponent<Renderer>();
            rend.sharedMaterial.mainTexture = handle.Result;
        }
    }

    void OnDestroy()
    {
        // Always release when the component is destroyed
        if (textureHandle.IsValid())
        {
            Addressables.Release(textureHandle);
        }
    }
}

A common Addressables leak happens when you load a texture, assign it to a UI Image, and then destroy the Image without releasing the Addressable handle. The texture stays loaded because the Addressables system still has a reference count of 1. Always pair loads with releases in your component lifecycle methods.

Resources.UnloadUnusedAssets and When to Use It

Resources.UnloadUnusedAssets() scans all loaded assets and releases any that have zero active references. It is the broad-spectrum cleanup tool for texture memory. However, it is expensive — it can take 100-500ms depending on how many assets are loaded — so you should only call it during loading screens or scene transitions, never during gameplay.

using UnityEngine;
using UnityEngine.SceneManagement;
using System;
using System.Collections;

public class SceneCleanup : MonoBehaviour
{
    public IEnumerator CleanTransition(string nextScene)
    {
        // Load the next scene additively first
        yield return SceneManager.LoadSceneAsync("LoadingScreen");

        // Null out any static references that hold textures
        TextureCache.ClearAll();

        // Unload assets with no remaining references
        yield return Resources.UnloadUnusedAssets();

        // Force garbage collection to release managed wrappers
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect(); // Second pass catches weak references from finalizers

        // Now load the target scene with clean memory
        yield return SceneManager.LoadSceneAsync(nextScene);
    }
}

// Example of a texture cache that supports cleanup
public static class TextureCache
{
    private static System.Collections.Generic.Dictionary<string, Texture2D> cache
        = new System.Collections.Generic.Dictionary<string, Texture2D>();

    public static Texture2D Get(string key)
    {
        cache.TryGetValue(key, out Texture2D tex);
        return tex;
    }

    public static void Set(string key, Texture2D tex)
    {
        cache[key] = tex;
    }

    public static void ClearAll()
    {
        // Destroy all cached textures before clearing references
        foreach (var kvp in cache)
        {
            if (kvp.Value != null)
            {
                UnityEngine.Object.Destroy(kvp.Value);
            }
        }
        cache.Clear();
    }
}

The double GC.Collect() call is intentional. The first pass collects objects with finalizers and queues them for finalization. The second pass, after WaitForPendingFinalizers(), collects the actual memory freed by those finalizers. Without the second pass, some texture wrapper objects may survive and keep their native textures alive for one more GC cycle.

Preventing Leaks with a Texture Lifecycle Pattern

The most reliable way to prevent texture leaks is to adopt a consistent ownership pattern: whoever creates a texture is responsible for destroying it. Wrap this pattern in a helper class to enforce it:

using UnityEngine;
using System;

/// Wraps a runtime Texture2D with automatic cleanup via IDisposable
public class ManagedTexture : IDisposable
{
    public Texture2D Texture { get; private set; }
    private bool disposed = false;

    public ManagedTexture(int width, int height,
        TextureFormat format = TextureFormat.RGBA32)
    {
        Texture = new Texture2D(width, height, format, false);
    }

    public void Dispose()
    {
        if (!disposed && Texture != null)
        {
            UnityEngine.Object.Destroy(Texture);
            Texture = null;
            disposed = true;
        }
    }
}

// Usage:
public class TextureUser : MonoBehaviour
{
    private ManagedTexture managedTex;

    void Start()
    {
        managedTex = new ManagedTexture(256, 256);
        // Use managedTex.Texture as needed
    }

    void OnDestroy()
    {
        managedTex?.Dispose();
    }
}

"If you created it with 'new', you own it. If you own it, you destroy it. No exceptions. This one rule eliminates 90% of texture memory leaks."

Related Issues

If your memory usage spikes during scene transitions rather than growing gradually, see our guide on fixing Unity scene loading freezes, which covers memory management during async loading. For games that crash on Android due to memory pressure, fixing Android build crashes covers OS-level memory limits and monitoring. And if your textures load correctly but appear visually wrong, fixing pink/magenta materials covers shader compilation issues that can affect texture rendering.

If you created it with 'new', you own it. If you own it, you Destroy it.