Reduce memory

Once you understand Android memory management and have set up tools to measure your game's memory use, the next step is to actively reduce and optimize it. Staying within Android's strict limits helps prevent the system from closing your game, stops long startup times, and ensures your game runs well on all devices.

This guide provides practical techniques to trim your game's memory footprint, specifically focusing on asset-level optimization, engine-specific configurations, and memory management best practices.

Reduce memory in Unity

Due to the architectural design of Unity, once native block allocators and the managed heap expand, the engine tends to retain those memory pages for reuse rather than immediately returning them to the operating system (OS), even after assets are released. Specifically, the virtual address space (reserved memory) remains reserved for the lifetime of the process, and physical memory (RSS) isn't reclaimed right away until several garbage collection (GC) and trimming cycles have occurred. Consequently, a temporary memory peak can cause resident memory to stay inflated for a long time even after actual usage drops. This behavior increases the risk of out-of-memory (OOM) crashes on low-end devices and degrades overall runtime stability.

Therefore, Unity memory optimization must be approached through three core pillars tailored to these engine behaviors:

  • Control active memory usage to prevent peak memory spikes in the first place.
  • Manage texture formats and shader variants to ensure unnecessary assets and native resources aren't instantiated.
  • Refactor runtime code structures to eliminate unnecessary allocations on the managed heap to minimize GC frequency and heap expansion.

For more information, see Unity memory optimization.

Multi-process optimization

Memory usage of cached processes is excluded from memory limits accounting because it has no impact on active apps. Running the service in a separate isolated process helps the main process transition to a cached state as quickly as possible, thereby improving the health of your game.

For more information, see how to track process state and memory, how to isolate a service process with Unity, and how to isolate a service process with Unreal.

Reduce memory usage in user-perceived services

Your game might need to run logic in a user-perceived service for use cases like completing a large download or for background voice chat systems. These strategies can help you manage and reduce memory usage during these scenarios.

Strategies for large downloads

These strategies might apply to large downloads that you want to continue even after the user has minimized your game.

1. Isolate the download process

What: Ensure that the OS can immediately reclaim memory your app isn't using by performing the download in a separate process, since it's possible the memory allocator may hold on to memory pool pages and keep Anonymous RSS memory artificially high even after freeing arrays. When you terminate or exit the service and process explicitly, the memory is returned to the OS pool and your main process will remain unaffected.

  • In Unity: Offload the download to a native Android Service declared with a process like android:process=":downloader" in a custom Manifest and invoke it with Unity's AndroidJavaClass JNI. Make sure to terminate the process when the download completes. See Run a perceptible service in a separate process with Unity for more detailed guidance.

  • In Unreal: Declare a custom Android Service with a process like android:process=":downloader" using Unreal Plugin Language, and trigger it with C++ JNI. Make sure to terminate the process when the download completes. See Run a perceptible service in a separate process with Unreal for more detailed guidance.

  • For Native Android: Declare a Service in the AndroidManifest with a process like android:process=":downloader". Run the download in this isolated process, and call Process.killProcess(Process.myPid()) when the download completes.

How this helps: Reduces the duration that memory is held, and allows the download to continue while releasing memory used by the larger main process.

2. Stream downloads directly to disk

What: Stream data directly from the network socket to the disk by using a small, fixed-size reusable buffer rather than accumulating network responses into a large array before writing it.

  • In Unity: Avoid using DownloadHandlerBuffer for asset bundles or large files, since it allocates a native memory buffer equivalent to the file size (Anonymous RSS memory). Instead, use DownloadHandlerFile to stream bytes natively to disk on a background thread.

  • In Unreal: Pipe incoming chunks of data directly into an FArchive (file-backed archive using Unreal's File Manager) with SetResponseBodyReceiveStream() rather than appending payloads from an IHttpRequest into a TArray<uint8>.

  • For Native Android: Pipe the InputStream to a FileOutputStream using a pooled buffer rather than calling .readBytes() or .string() on an HTTP response.

How this helps: Reduces peak memory usage

3. Stream downloaded file decompression

What: If your download is compressed, wrap your network input stream in a streaming decompressor like ZipInputStream rather than downloading the file, loading it into RAM, and then extracting it.

How this helps: Reduces peak memory usage

4. Delegate to the OS

What: To avoid managing background memory altogether, delegate the work to Android's native APIs.

  • WorkManager is the modern, recommended wrapper around the OS-level JobScheduler. For Android 14+, WorkManager automatically handles user-triggered downloads as a User-Initiated Data Transfer (UIDT) job. This runs inside your app's process, so you must still stream directly to disk to minimize memory usage. UIDT protects your app from Low Memory crashes by allowing the OS to gracefully pause and resume your download if system resources become limited.

  • DownloadManager runs in a separate system process and doesn't attribute memory usage for the download to your app. Your app will receive a broadcast notification when the file is downloaded and ready.

How this helps: WorkManager helps deal with low memory scenarios, and DownloadManager reduces your app's memory usage.

Strategies for auxiliary services

These strategies might apply to auxiliary services that a game runs in parallel with the main game process, such as background voice chat.

1. Isolate the process

What: Decouple the feature, for example your voice chat solution, from the main game engine. For example, you could run the microphone capture and network streaming inside an Android foreground service assigned to a separate process (declared in the Manifest, such as with android:process=":voice").

How this helps: When the app is minimized, the heavy main game process can drop into a lower-priority cached state while the lighter-weight auxiliary service continues in user-perceived service state.

2. Trim unused in-process memory

What: If the auxiliary service is too deeply integrated into the game engine to separate, try to shed as much weight from the process as soon as the game is backgrounded/minimized. Consider flushing texture caches, unloading non-essential scenes, dropping the engine tick and render rates to 0, and explicitly calling garbage collection.

How this helps: Reduces memory usage that isn't needed when the game isn't foregrounded.