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 state | Memory 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 state | Budget bucket |
|---|---|
PERSISTENT | Foreground |
PERSISTENT_UI | Foreground |
TOP | Foreground |
BOUND_TOP | Foreground |
TOP_SLEEPING | Foreground |
FOREGROUND_SERVICE | Perceptible |
BOUND_FOREGROUND_SERVICE | Perceptible |
IMPORTANT_FOREGROUND | Perceptible |
IMPORTANT_BACKGROUND | Perceptible |
TRANSIENT_BACKGROUND | Background |
BACKUP | Background |
SERVICE | Background |
RECEIVER | Background |
HEAVY_WEIGHT | Background |
HOME | Background |
LAST_ACTIVITY | Background |
CACHED_ACTIVITY | Background |
CACHED_ACTIVITY_CLIENT | Background |
CACHED_RECENT | Background |
CACHED_EMPTY | Background |
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. minimumRequiredMemTotalThe 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
MemTotalvalue 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
MemTotalis lower than the device's advertised physical DRAM,minimumRequiredMemTotalthresholds include a margin for these carve-outs. For example, a configuration targeting 16 GB devices specifies a floor of14336(14 GiB) rather than16384(16 GiB), and a tier for 12 GB devices specifies a floor of9216(9 GiB). This ensures that each device matches its intended tier regardless of board-level carve-out variations.maximumAllowedMemTotalThe maximum system
MemTotal(in MiB, exclusive ceiling) for this limit set to apply. IfMemTotalis 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.memVisibleThe
memory.maxhard memory limit in MiB applied to the package UID cgroup when the package has a visible process. The defaultmemory.highsoft reclaim threshold is 90% of this value, rounded to the nearest MiB.memNotVisibleThe
memory.maxhard memory limit in MiB applied to the package UID cgroup when the package has no visible processes. The defaultmemory.highsoft reclaim threshold is 90% of this value, rounded to the nearest MiB.swapVisibleThe
memory.swap.maxswap space limit in MiB applied to each visible process PID cgroup.swapNotVisibleThe
memory.swap.maxswap 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.minprotection floors on critical interactive daemons and tighter, workload-calibratedmemory.highceilings 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.highlimit, 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 statusRunning 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:
limitsandkilling- Indicate whether the
memory.highandmemory.maxplatform memory limits are configured and enforced for the device's RAM tier. ignored- The UID excluded by the
ignoresub-command,allwhen limiting is globally paused, ornone. visibleandnot-visible- The default UID
memHighlimit formemory.high, UIDmemMaxlimit formemory.max, and per-processswapMaxlimit formemory.swap.maxenforced for each visibility classification. active- The number of tracked process and UID limiters in
system_server. uid-started,uid-current, anduid-updated- The cumulative started count, active count, and cumulative replacement count for UID limiters updated with manifest budgets.
watchedandwatch-failed- The cumulative number of cgroup
memory.eventswatches registered and failed. limitersandlimiters-hwm- The number and high-water mark of active native cgroup watchers.
eventsandfalse-events- The counts of over-limit events dispatched to
system_serverandmemory.eventsnotifications 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.