Memory Limiter

Android 17 and higher includes the Memory Limiter, a system service that monitors and limits the memory usage of application processes using Linux cgroup v2. The Memory Limiter prevents individual apps from consuming disproportionate system memory, which reduces overall memory pressure and prevents aggressive low-memory kills of critical foreground and cached processes.

Mechanism

The Memory Limiter integrates with the Activity Manager Service (AMS) to track process lifecycle events and state changes. The Memory Limiter enforces memory limits using the Linux kernel cgroup v2 hierarchy.

To support the Memory Limiter, the device kernel must enable cgroup v2 and the memory controller. The service specifically relies on the following attributes:

memory.high
A soft reclaim threshold applied to the package UID cgroup, or to a process PID cgroup when a per-process memory budget is active. Defaults to 90% of the platform memory limit unless tightened by a memory budget. When exceeded, the kernel throttles allocations and reclaims pages from that cgroup.
memory.max
A hard memory ceiling applied to the package UID cgroup at 100% of the platform memory limit. If reclaim can't keep memory usage under memory.max, the kernel terminates the out-of-memory process.
memory.swap.max
Limits the maximum swap space, such as ZRAM, that each process PID cgroup can use.

Impact on apps

Apps that operate within their memory limits aren't affected by the Memory Limiter.

When an app crosses its memory.high limit, the kernel evicts the app's clean file-backed memory and swaps out its inactive anonymous memory to keep the app within the limit. As a result of this page eviction and swap activity, the app might experience temporary execution throttling, but it continues to run.

In extreme situations, such as an uncontrolled memory leak where the app continues allocating memory past memory.max or after swap space is exhausted, the app fails to allocate memory and is terminated.

Process monitoring

The Memory Limiter applies the platform limits on this page to app processes (UID >= 10000). Processes with a UID below 10000 don't receive platform limits, and neither does the package of the default on-device sandboxed inference service (config_defaultOnDeviceSandboxedInferenceService). System apps and services can still declare tighter memory budgets, as described in System memory budgets.

The Memory Limiter assigns memory limits based on the visibility state of the process:

  • Visible processes: Processes hosting a user-visible UI, such as the focused activity or an active interaction. Because displaying a UI requires a larger working set for rendering and graphics pipelines, visible processes receive a more generous memory limit.

  • Not visible processes: Processes performing work in the background without presenting a UI, such as background services or broadcast receivers. Because they don't render UI components, they operate under a more restrictive limit.

The following table maps specific process states to platform memory limit classifications:

Process stateMemory limit
PERSISTENT Unrestricted
PERSISTENT_UI Unrestricted
TOP visible
BOUND_TOP visible
FOREGROUND_SERVICE not-visible
BOUND_FOREGROUND_SERVICE not-visible
IMPORTANT_FOREGROUND visible
IMPORTANT_BACKGROUND not-visible
TRANSIENT_BACKGROUND not-visible
BACKUP not-visible
SERVICE not-visible
RECEIVER not-visible
TOP_SLEEPING visible
HEAVY_WEIGHT not-visible
HOME not-visible
LAST_ACTIVITY not-visible
CACHED_ACTIVITY not-visible
CACHED_ACTIVITY_CLIENT not-visible
CACHED_RECENT not-visible
CACHED_EMPTY not-visible

PERSISTENT and PERSISTENT_UI processes don't receive a platform limit. If one of these processes declares a memory budget, its foreground budget applies, as described in Memory budget buckets.

In the cached state, processes retain the not-visible platform limit alongside cached-app freezing and compaction, keeping them bounded even if freezing or maximal reclamation doesn't occur.

When a process exceeds its assigned memory.high limit, the Memory Limiter detects the event and can trigger diagnostic actions, such as capturing a memory profile or logging an anomaly to statsd.

Memory budget buckets

On devices that support memory budgets, apps and system services can declare budgets that are tighter than the platform limits. Apps declare budgets in their manifest, as described in Set app memory budgets. For system apps and services, see System memory budgets. A budget sets memory.high and can't exceed the platform limit for the same process state.

When a process has a budget, the Memory Limiter maps its process state to one of three budget buckets. The manifest android:state values foreground, perceptible, and background specify the budget for the matching bucket. Cached processes in states CACHED_ACTIVITY through CACHED_EMPTY retain the background budget alongside cached-app freezing and compaction.

Process stateBudget bucket
PERSISTENTForeground
PERSISTENT_UIForeground
TOPForeground
BOUND_TOPForeground
TOP_SLEEPINGForeground
FOREGROUND_SERVICEPerceptible
BOUND_FOREGROUND_SERVICEPerceptible
IMPORTANT_FOREGROUNDPerceptible
IMPORTANT_BACKGROUNDPerceptible
TRANSIENT_BACKGROUNDBackground
BACKUPBackground
SERVICEBackground
RECEIVERBackground
HEAVY_WEIGHTBackground
HOMEBackground
LAST_ACTIVITYBackground
CACHED_ACTIVITYBackground
CACHED_ACTIVITY_CLIENTBackground
CACHED_RECENTBackground
CACHED_EMPTYBackground

Configuration

In Android 17 and higher, the Memory Limiter is configured using the standard platform configuration file on the system partition at /system/etc/memory-limiter-config.xml.

XML format

The configuration file follows the schema defined in memory-limiter-config.xsd. The file defines multiple limit sets ordered by available memory; the service selects the highest matching limit set based on the device's available RAM. All memory values are specified in mebibytes (MiB).

<MemoryLimiterConfig>
  <version>1</version>
  <configList>
    <!-- RAM boundaries account for MemTotal excluding carve-outs. -->
    <limitSet>
      <!-- Limits for 16GB RAM device [14336, 18432): 10G/5G/5G/5G -->
      <minimumRequiredMemTotal>14336</minimumRequiredMemTotal>
      <maximumAllowedMemTotal>18432</maximumAllowedMemTotal>
      <memVisible>10240</memVisible>
      <memNotVisible>5120</memNotVisible>
      <swapVisible>5120</swapVisible>
      <swapNotVisible>5120</swapNotVisible>
    </limitSet>
    <limitSet>
      <!-- Limits for 12GB RAM device [9216, 14336): 8G/4G/4G/4G -->
      <minimumRequiredMemTotal>9216</minimumRequiredMemTotal>
      <maximumAllowedMemTotal>14336</maximumAllowedMemTotal>
      <memVisible>8192</memVisible>
      <memNotVisible>4096</memNotVisible>
      <swapVisible>4096</swapVisible>
      <swapNotVisible>4096</swapNotVisible>
    </limitSet>
    <limitSet>
      <!-- Limits for 8GB RAM device [6800, 9216): 5G/3G/3G/3G -->
      <minimumRequiredMemTotal>6800</minimumRequiredMemTotal>
      <maximumAllowedMemTotal>9216</maximumAllowedMemTotal>
      <memVisible>5120</memVisible>
      <memNotVisible>3072</memNotVisible>
      <swapVisible>3072</swapVisible>
      <swapNotVisible>3072</swapNotVisible>
    </limitSet>
    <limitSet>
      <!-- Limits for 6GB RAM device [4800, 6800): 4G/2G/2G/2G -->
      <minimumRequiredMemTotal>4800</minimumRequiredMemTotal>
      <maximumAllowedMemTotal>6800</maximumAllowedMemTotal>
      <memVisible>4096</memVisible>
      <memNotVisible>2048</memNotVisible>
      <swapVisible>2048</swapVisible>
      <swapNotVisible>2048</swapNotVisible>
    </limitSet>
    <limitSet>
      <!-- Limits for 4GB RAM device [3200, 4800): 2G/1G/1G/1G -->
      <minimumRequiredMemTotal>3200</minimumRequiredMemTotal>
      <maximumAllowedMemTotal>4800</maximumAllowedMemTotal>
      <memVisible>2048</memVisible>
      <memNotVisible>1024</memNotVisible>
      <swapVisible>1024</swapVisible>
      <swapNotVisible>1024</swapNotVisible>
    </limitSet>
  </configList>
</MemoryLimiterConfig>
version
A positive integer identifying the configuration version. This must be 1.
minimumRequiredMemTotal

The minimum system MemTotal (in MiB, inclusive floor) required for this limit set to apply. The service evaluates this against the total available memory reported by the kernel in /proc/meminfo.

Understanding memory carve-outs and boundaries: The kernel MemTotal value reflects the physical DRAM accessible to the operating system after hardware and firmware memory carve-outs are deducted. Carve-outs reserve RAM for specialized hardware components such as the GPU, baseband modem, camera ISP, secure execution environments, and hypervisors, typically consuming between 500 MiB and over 2 GiB of physical memory.

Because MemTotal is lower than the device's advertised physical DRAM, minimumRequiredMemTotal thresholds include a margin for these carve-outs. For example, a configuration targeting 16 GB devices specifies a floor of 14336 (14 GiB) rather than 16384 (16 GiB), and a tier for 12 GB devices specifies a floor of 9216 (9 GiB). This ensures that each device matches its intended tier regardless of board-level carve-out variations.

maximumAllowedMemTotal

The maximum system MemTotal (in MiB, exclusive ceiling) for this limit set to apply. If MemTotal is greater than or equal to this ceiling, the limit set doesn't apply. For devices with 18,432 MiB or more or for devices with less than 3,200 MiB, the Memory Limiter doesn't apply limits, leaving processes unconstrained.

memVisible

The memory.max hard memory limit in MiB applied to the package UID cgroup when the package has a visible process. The default memory.high soft reclaim threshold is 90% of this value, rounded to the nearest MiB.

memNotVisible

The memory.max hard memory limit in MiB applied to the package UID cgroup when the package has no visible processes. The default memory.high soft reclaim threshold is 90% of this value, rounded to the nearest MiB.

swapVisible

The memory.swap.max swap space limit in MiB applied to each visible process PID cgroup.

swapNotVisible

The memory.swap.max swap space limit in MiB applied to each not-visible process PID cgroup.

Standard platform limits

In Android 17 and higher, the platform specifies standard memory and swap limits in /system/etc/memory-limiter-config.xml across common physical RAM tiers using [floor, ceiling) range intervals:

Target physical RAM MemTotal range [floor, ceiling)
(minimumRequiredMemTotal,
maximumAllowedMemTotal)
Visible memory limit
(memVisible)
Not visible memory limit
(memNotVisible)
Visible swap limit
(swapVisible)
Not visible swap limit
(swapNotVisible)
16 GB [14336, 18432) MiB 10,240 MiB (10 GiB) 5,120 MiB (5 GiB) 5,120 MiB (5 GiB) 5,120 MiB (5 GiB)
12 GB [9216, 14336) MiB 8,192 MiB (8 GiB) 4,096 MiB (4 GiB) 4,096 MiB (4 GiB) 4,096 MiB (4 GiB)
8 GB [6800, 9216) MiB 5,120 MiB (5 GiB) 3,072 MiB (3 GiB) 3,072 MiB (3 GiB) 3,072 MiB (3 GiB)
6 GB [4800, 6800) MiB 4,096 MiB (4 GiB) 2,048 MiB (2 GiB) 2,048 MiB (2 GiB) 2,048 MiB (2 GiB)
4 GB [3200, 4800) MiB 2,048 MiB (2 GiB) 1,024 MiB (1 GiB) 1,024 MiB (1 GiB) 1,024 MiB (1 GiB)

Memory limit principles

The Memory Limiter configuration is designed around the following platform principles:

  • Ecosystem consistency and app compatibility: Consistent memory limits across devices ensure predictable app performance throughout the Android ecosystem. Apps are developed and tested against standard memory expectations; maintaining uniform platform limits avoids unexpected throttling or premature terminations.

  • Proportional resource allocation: The platform limits are calibrated according to physical RAM capacity:

    • Visible processes: Allocated approximately 1/2 to 2/3 of total physical RAM to support active UI, composition, and rendering workloads.
    • Not visible processes: Allocated approximately 1/4 to 1/3 of total physical RAM for background tasks.
  • Universal applicability: Limits apply uniformly to all application processes on the device (UID >= 10000), including preinstalled system and OEM apps. Apart from the default on-device sandboxed inference service, the Memory Limiter doesn't exempt individual apps, ensuring fair memory governance across all apps. To configure self-tuning memory.min protection floors on critical interactive daemons and tighter, workload-calibrated memory.high ceilings on individual system apps and services, see System memory budgets.

  • Absence of runtime query APIs: In Android 17 and higher, apps can't programmatically query their assigned memory limits at run time. The standard platform limits are defined with generous margins so that well-behaved apps operate without constraints during normal usage.

  • Kernel reclaim and swap dynamics: When an app process approaches its memory.high limit, the Linux kernel directs memory reclaim specifically at that process cgroup. This includes dropping inactive file-backed pages and swapping cold anonymous pages to ZRAM, maintaining system fluidity without imposing global memory pressure.

Shell commands

The am memory-limiter command lets developers and system integrators interact with the service at run time for development, testing, and debugging:

am memory-limiter <SUB-COMMAND>

status

The status sub-command reports the operational status, configured platform limits, and active monitoring counters of the Memory Limiter:

adb shell am memory-limiter status

Running adb shell am memory-limiter status version prints 2 when the two-threshold Memory Limiter is enabled, or 0 when disabled.

The following is example status output on an 8 GB device:

Memory limiter
  enabled           limits=true       killing=true      ignored=none
  visible:          memHigh=4608MB    memMax=5120MB     swapMax=3072MB
  not-visible:      memHigh=2765MB    memMax=3072MB     swapMax=3072MB
  active=64         uid-started=32    uid-current=28    uid-updated=4
  started=72        watched=72        watch-failed=0
  limiters=64       limiters-hwm=70   red-poll=0        red-limiter=0
  events=0          false-events=0    modify=84

Key fields in the output include:

limits and killing
Indicate whether the memory.high and memory.max platform memory limits are configured and enforced for the device's RAM tier.
ignored
The UID excluded by the ignore sub-command, all when limiting is globally paused, or none.
visible and not-visible
The default UID memHigh limit for memory.high, UID memMax limit for memory.max, and per-process swapMax limit for memory.swap.max enforced for each visibility classification.
active
The number of tracked process and UID limiters in system_server.
uid-started, uid-current, and uid-updated
The cumulative started count, active count, and cumulative replacement count for UID limiters updated with manifest budgets.
watched and watch-failed
The cumulative number of cgroup memory.events watches registered and failed.
limiters and limiters-hwm
The number and high-water mark of active native cgroup watchers.
events and false-events
The counts of over-limit events dispatched to system_server and memory.events notifications that didn't cross a monitored threshold.

ignore

The ignore sub-command temporarily excludes a specific UID or all processes from memory limiting. This is useful for performance benchmarking, stress testing, or diagnosing memory behavior:

# Ignore a specific UID
adb shell am memory-limiter ignore 10087
# Ignore all processes (temporarily disables limiting)
adb shell am memory-limiter ignore all
# Resume normal limiting operation
adb shell am memory-limiter ignore none

manual

The manual sub-command overrides the calculated memory.high, memory.max, and memory.swap.max limits with a custom integer ceiling up to the target's configured limits. Pass a PID, with or without -p, to target a single process, or pass -u <UID> to target all processes in a package UID. Specify the size in bytes, append MB for MiB or GB for GiB, or pass none to clear an override. For backward compatibility, bare integers under 20 without a suffix are interpreted as MiB:

# Set a 1 GiB limit for PID 1234, with or without -p
adb shell am memory-limiter manual 1234 1073741824
adb shell am memory-limiter manual 1234 1024MB
adb shell am memory-limiter manual -p 1234 1GB
# Set a 1 GiB limit for all processes in UID 10087
adb shell am memory-limiter manual -u 10087 1GB
# Remove the manual override for PID 1234 or UID 10087
adb shell am memory-limiter manual 1234 none
adb shell am memory-limiter manual -u 10087 none

Manual process overrides apply only for the lifetime of that specific process instance. If the process restarts, it returns to the standard limits based on its state. In contrast, manual -u <UID> overrides persist while the UID remains active, until all processes in the UID exit or you clear the override with none.

A manual override can't exceed physical system limits.