App memory budgets let apps declare a memory budget for themselves, which tells the system to trim their memory use when the app uses more than its set budget. This is especially useful for system and bundled apps or for apps targeting memory-constrained devices, where the developer knows their expected memory working set (the memory the app actively needs for what the user is currently doing) and wants to ensure their app doesn't use too much of the system's shared RAM resources.
The budget is kept balanced by using memory eviction and swap to remove memory pages that weren't recently used, focusing the app's memory footprint on its current working set. When an app exceeds its declared budget, the operating system targets reclaim specifically at that app:
- Clean file-backed pages (such as inactive code and mapped assets) are evicted first because they can be re-read from storage if needed.
- Dirty file-backed pages are written back to storage and evicted.
- Anonymous memory pages (such as heap allocations) are compressed and swapped to zRAM.
As long as the working set doesn't exceed the budget, the app will work well while using no more memory than is in its set budget. The operating system evicts unused memory and compresses inactive heap pages to swap so that memory allocations remain bounded without terminating the process.
What happens when an app exceeds its budget
Exceeding a memory budget doesn't cause the system to terminate or crash your app. Instead, when an app uses more memory than its budget allows, the OS finds memory that the app hasn't used in some time and safely puts it away until it's needed again. This makes way for new app memory allocations and keeps the overall app's memory use within budget.
As long as there is headroom between what the app needs at any given time (its working set) and the set budget, the app continues to run normally. If an app sets a budget that is smaller than its working set, the app's performance at runtime can slow down as a result.
To learn how the OS tracks and reclaims memory, see the memory architecture guide:
- Memory reclaim and swap: Explains how the OS charges private anonymous heap and file-backed pages to your app and reclaims cold pages when over budget.
- Fundamental concepts: Explains why shared Zygote memory is excluded from your app's budget and how resident budgets relate to anonymous RSS + swap in field telemetry.
- Bitmaps and memory: Explains how GPU-backed hardware bitmaps and DMA-BUF allocations are accounted outside your app's heap and memory budget.
Declare budgets in the Android manifest
Declaring your memory budgets in your AndroidManifest.xml is the primary and
recommended method for defining budgets. It requires no runtime code, takes
effect immediately upon process startup, and provides a clear contract for the
operating system.
<memory-budget> declarations take effect on devices running Android 17 QPR2
(API level 37.2) and higher. On lower Android versions, the platform manifest
parser safely ignores unrecognized XML elements, so you can adopt
<memory-budget> without affecting backward compatibility.
Declare a baseline budget
For most apps, defining a single budget for the application is all that is
needed. Declare a <memory-budget> element directly inside the <application>
tag:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.simpleapp">
<application
android:label="@string/app_name">
<!-- Baseline budget for the application -->
<memory-budget android:maxMb="256" />
</application>
</manifest>
This sets a 256MB resident memory budget across all processes and states for the package. When the app's memory footprint exceeds 256MB, the operating system trims inactive memory pages using eviction and swap.
Vary budgets by process state
An app requires different amounts of memory depending on its user visibility:
- Foreground: The process is hosting a visible Activity interacting with the user. This state typically has the largest footprint due to active UI and graphics.
- Perceptible: The process is perceptible to the user but doesn't host a visible window (for example, hosting a media playback foreground service, an active background download, turn-by-turn navigation, or an active input method).
- Background: The process is running background jobs, receivers, or data syncs. It is expected to maintain a minimal footprint.
You can declare multiple <memory-budget> clauses to match these states:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.simpleapp">
<application
android:label="@string/app_name">
<!-- Default budget for visible foreground UI -->
<memory-budget android:maxMb="200" />
<!-- Tighter budget when playing audio in background -->
<memory-budget
android:maxMb="120"
android:state="perceptible" />
<!-- Minimal budget when fully in background -->
<memory-budget
android:maxMb="48"
android:state="background" />
</application>
</manifest>
A base clause without android:state isn't required; if you only specify
state-specific clauses (for example, android:state="background"), other states
remain unconstrained by an app budget. When included, a clause without
android:state acts as the default fallback for unspecified states (such as
foreground), which subsequent more restrictive clauses override when the app
transitions into perceptible or background states.
When sizing budgets for each state, target the active working set of that state rather than the peak resident memory (RSS high-water mark):
- Size
foregroundbudgets for smooth frame rendering: Give foreground UI enough headroom for its active working set so synchronous reclaim doesn't delay frame rendering. If your app includes a memory-intensive standalone workflow (such as a high-resolution image or video editor), consider moving that activity into a dedicated process (see Multi-process apps) so its allocations are budgeted independently and torn down when the user exits. - Size
backgroundbudgets tightly for headless work: High background RSS in field telemetry often reflects UI allocations retained after leaving the foreground (see Co-locating a bound service with a memory-intensive UI and Release memory in response to events) or one-time file reads during background indexing and syncs. Because headless background work has no frame deadlines, evicting one-time file cache pages and swapping inactive heap pages to zRAM when a background sync briefly allocates beyond its budget causes no user-visible jank while preventing background spikes from evicting the user's foreground app.
For a step-by-step workflow, see Measure your working set and calibrate budgets.
How budget states map to process states
The platform maps manifest budget states to runtime process states based on
RunningAppProcessInfo.importance, which
Activity Manager computes from both the process's own active components and any
incoming service bindings or content provider
queries from other apps:
foreground: Active, visible user interactions, such as hosting a resumed Activity, maintaining the top app state when the screen turns off, or hosting a service or content provider actively bound or queried by the top foreground app.perceptible: Workloads that are perceptible to the user without a visible window, such as active media playback, turn-by-turn navigation, camera or microphone capture, active background downloads, data sync foreground services, or services bound by a foreground client using flags such asBIND_NOT_FOREGROUND(see Controlling inheritance with BIND flags). While internal platform metrics might label some of these workloadsPROCESS_STATE_IMPORTANT_FOREGROUND, that internal constant doesn't signify a visible window and is governed by theperceptiblebudget.background: Work that isn't immediately perceptible to the user, such as background jobs, alarms, and broadcast receivers.
The following table shows how runtime importance levels map to manifest states:
Manifest android:state |
Runtime importance (RunningAppProcessInfo) |
Typical components |
|---|---|---|
foreground |
IMPORTANCE_FOREGROUNDIMPORTANCE_TOP_SLEEPING |
Resumed visible Activity, top app with screen locked, service or content provider bound by the top app |
perceptible |
IMPORTANCE_FOREGROUND_SERVICEIMPORTANCE_VISIBLE |
Active media playback, navigation, downloads, or sync foreground services; service bound with foreground-service or visible flags |
background |
IMPORTANCE_PERCEPTIBLEIMPORTANCE_CANT_SAVE_STATEIMPORTANCE_SERVICE |
Background jobs, receivers, background sync |
For details on how client bindings elevate a service or provider host process
and how the process transitions back to background or cached after the
client unbinds or moves off-screen, see Service bindings and process
states.
When a process enters a cached state (IMPORTANCE_CACHED) with no active
components, the platform releases its active memory budget and manages its
memory through standard cached-process reclamation.
Inspect your app's process state and budget
One way to inspect your app's active process state and importance during development is by querying Activity Manager using ADB:
adb shell dumpsys activity processes <package-name>
The following abbreviated example shows the process record and OOM control entries for an app running a foreground service:
ACTIVITY MANAGER RUNNING PROCESSES (dumpsys activity processes)
All known processes:
*APP* UID 10123 ProcessRecord{edf056c 3919:com.example.app/u0a123}
pid=3919
oom adj: max=1001 curRaw=200 setRaw=200 cur=200 set=200
curProcState=4 mRepProcState=4 setProcState=4 lastStateTime=-42s360ms
hasStartedServices=true
mHasForegroundServices=true forcingToImportant=null
...
Process OOM control (48 total):
Proc #22: prcp F/S/FGS ---NFU-TI t: 0 3919:com.example.app/u0a123 (fg-service)
oom: max=1001 curRaw=200 setRaw=200 cur=200 set=200
state: cur=FGS set=FGS lastRss=0.00 lastCachedRss=0.00
In this output:
curProcState=4andstate: cur=FGSreflect that the process is in the foreground service state. If your app is hosting an active visible activity, this appears asTOP. If it is running an important foreground component (such as a download or sync), it appears asIMPF.- In the Process OOM control table,
prcpindicates that the process is evaluated under the perceptible priority tier, which corresponds to theperceptiblebudget.
To inspect your app's current resident memory usage:
adb shell dumpsys meminfo <package-name>
Multi-process apps
If your application divides its work across multiple processes, configure
dedicated process budgets using the <process> tag inside <processes>.
For example, consider a streaming music app (com.example.radio):
- Main process: Hosts the visible UI and the audio playback engine
(
MediaSessionServicewith amediaPlaybackforeground service). When visible, the process operates under the 180MB foreground budget. When the user leaves the app while music continues playing, the process enters theperceptiblestate, where a 64MB budget is sufficient for the playback engine and audio buffer. - Sync process (
:sync): Dedicated process running background metadata synchronization and download indexing. Because this process is only ever active in the background, you don't need to explicitly declarestate="background"; a single budget applies.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.radio">
<application
android:label="@string/app_name">
<!-- Package baseline: main process with UI and audio playback -->
<memory-budget android:maxMb="180" />
<!-- Tighter budget when audio plays in the background -->
<memory-budget
android:maxMb="64"
android:state="perceptible" />
<processes>
<!-- Dedicated background sync process -->
<process android:process=":sync">
<memory-budget android:maxMb="32" />
</process>
</processes>
<service
android:name=".playback.AudioPlayerService"
android:foregroundServiceType="mediaPlayback"
android:exported="false" />
<service
android:name=".sync.PlaylistSyncService"
android:process=":sync"
android:exported="false" />
</application>
</manifest>
When you declare a <processes> block in your manifest, list <process>
entries only for the processes that declare a <memory-budget>. If you want to
budget only specific subprocesses while leaving the rest of the app at the
system limit for the device, you can omit the <memory-budget> elements
directly under <application>.
Memory use in any subprocess counts against both its process budget and the enclosing package budget. A process experiences memory pressure at runtime if it breaches either its process budget or the package budget, whichever threshold is reached first.
Scale budgets for high-density displays
For applications whose memory footprint scales significantly with how many pixels there are to draw on the display at once—such as a photo gallery app caching bitmaps sized to the screen—Android provides two alternative mechanisms to scale budgets dynamically with display specifications:
Scale by display density bucket (
android:additionalMbPerDensity): Adds megabytes proportionally to the square of the display's density ratio relative tomdpi(1.0x / 160 dpi), because 2D raster drawables and UI assets scale across both width and height:<!-- Baseline 180MB + 16MB multiplied by (density ratio)^2 --> <memory-budget android:maxMb="180" android:additionalMbPerDensity="16" />On an
mdpidisplay (1.0x), the budget is 180 + 16 × 12 = 196 MB. On anxxhdpidisplay (3.0x), the budget scales to 180 + 16 × 32 = 324 MB.Scale by physical display resolution (
android:additionalBytesPerDisplayPixel): Adds bytes directly per physical display pixel (Width × Height). This is ideal for applications allocating full-screen graphics surfaces, render buffers, or full-resolution photo caches where memory consumption scales directly with raw display pixel count rather than UI density buckets:<!-- Baseline 128MB + 16 bytes per physical display pixel --> <!-- For example, a 4-byte RGBA full-screen buffer with double or quadruple buffering --> <memory-budget android:maxMb="128" android:additionalBytesPerDisplayPixel="16" />On a 1080p display (1080 × 2400 ≈ 2.59M pixels), this adds ≈ 41.4 MB to the baseline budget. On a 1440p display (1440 × 3120 ≈ 4.49M pixels), it adds ≈ 71.8 MB.
These two attributes are alternatives. Choose the attribute that matches your app's primary scaling factor, and avoid combining both in the same clause.
Specialize for device form factors
When shipping an APK across phones, tablets, and Wear OS, use the
android:feature attribute to adjust budgets for different hardware targets.
On Wear OS watches, RAM is constrained, and the app's UI and feature set is
much simpler. You can declare a tighter budget specialized for the watch
feature:
<!-- General phone and tablet baseline -->
<memory-budget android:maxMb="180" />
<!-- Wear OS override: simpler UI and constrained hardware -->
<memory-budget
android:maxMb="48"
android:feature="watch" />
Resolution rule: last applicable clause takes effect
When defining multiple <memory-budget> elements for an application or
process, the system evaluates them in the order they are declared in the
manifest. The last applicable budget clause is the one that is enforced.
Because the last applicable budget wins, ordering matters. Always place the most general baseline budget first, followed by more specific overrides (such as state-specific or hardware-specific clauses).
XML attribute reference
All memory size attributes are expressed in megabytes (MB) and map to Linux
cgroup memory.current charge (which excludes shared memory like the Zygote).
| Attribute | Format | Default | Description |
|---|---|---|---|
android:maxMb |
Integer (> 0) | Required | The baseline resident memory budget limit in MB. |
android:state |
Enum | Any | The process state this budget applies to: foreground, perceptible, or background. If omitted, the clause acts as a fallback for any unspecified state. |
android:additionalMbPerDensity |
Integer (≥ 0) | 0 |
Additional megabytes multiplied by the square of the display density ratio relative to mdpi (1.0x). |
android:additionalBytesPerDisplayPixel |
Integer (≥ 0) | 0 |
Additional bytes allocated per physical display pixel (Width × Height), useful for surface buffers and bitmaps. |
android:feature |
String | Any | Restricts the clause to devices declaring specific hardware features: watch, automotive, or leanback. |
Measure your working set and calibrate budgets
A good budget sits just above your app's working set for a given state, not at its unconstrained peak. To find that point, run a realistic user flow, lower the budget step by step, and watch for the moment your app starts paying for it.
Understand working set versus resident memory
Without a budget, your app's resident footprint (memory.current) holds two
kinds of memory:
- The working set: the Java, Kotlin, and native heap and the hot file-backed pages (DEX code, native libraries, and assets) that the current user flow touches over and over
- Cold slack: bitmap caches from earlier screens, one-time startup buffers, heap slack left over after an allocation burst, and file pages that were read once and never touched again
A budget at your unconstrained peak never reclaims the cold slack. A budget below your working set makes the kernel swap out heap pages and evict code pages that your app immediately faults back in. These working-set refaults cost CPU time and battery, and once they land on the UI thread, they cost frames too.
What you're looking for is the knee: the lowest budget where reclaim removes only cold slack, so refaults, CPU time, and frame times stay where they were without a budget.
Prepare your test environment
Set up a repeatable test that behaves like production:
- Calibrate the same R8-optimized, non-debuggable release build that you ship. Debug builds use more memory, so a budget tuned on a debug build is too high for release.
- Test on a device or emulator running Android 17 QPR2 (API level 37.2) or
higher. A
userdebugbuild or an emulator image without Google Play is best, because some devices only let you read the cgroup files in the following sections after you runadb root. - Script a 30 to 60 second user flow with Jetpack
Macrobenchmark, UI Automator, or
adb shell input, once for each state that you plan to budget. An idle app tells you nothing about its working set.foreground: scroll dense lists and media grids, move between your main destinations, open menus and dialogs, and run animationsperceptible: leave the visible window while perceptible work continues (for example, audio playback or a download), let it run for 10 to 15 seconds, and then return to the appbackground: trigger a sync, aWorkManagerjob (for example, withadb shell cmd jobscheduler run -f <package-name> <job-id>), a started service, or a broadcast receiver while the app has no visible UI
Measure a background budget while the background work is running. Once the
process becomes cached, the platform releases its budget (see How budget
states map to process states), so an idle cached
process doesn't tell you anything about your background budget.
Measure your baseline
Start with runs that have no budget at all, so you know what normal looks like.
Use a build without <memory-budget> declarations, and start each run from a
fresh process (adb shell am force-stop <package-name>).
Each app process has its own cgroup v2 memory controller. Look up its path from
the 0:: line of /proc/<PID>/cgroup instead of building the path yourself,
because the cgroup layout can differ between devices. Pass each device command
to adb shell as a single quoted string so that the device shell sees the
quotes:
PID=$(adb shell pidof -s <package-name>)CG=/sys/fs/cgroup$(adb shell "sed -n 's/^0:://p' /proc/$PID/cgroup")adb shell "cat $CG/memory.current $CG/memory.swap.current"adb shell "grep -E '^(anon|file|workingset_refault_.*) ' $CG/memory.stat"
Reading these files is cheap and doesn't disturb your app, so you can sample them every second during a run:
memory.current: resident memory charged to the process, in bytes. This is the number that<memory-budget>limits.memory.swap.current: memory compressed into zRAM swap, in bytesanonandfileinmemory.stat: resident anonymous and file-backed memory, in bytesworkingset_refault_anonandworkingset_refault_fileinmemory.stat: running counts of swapped-out or evicted pages that your app needed again. These counters are your main sign that a budget is too tight.
If you get a permission error, run adb root and try again. If you can't get
root, adb shell dumpsys meminfo <package-name> gives you a coarser view. In
App Summary, Java Heap, Native Heap, and Stack are mostly anonymous
memory, and Code is file-backed. Software bitmaps count toward Native Heap
and your budget, while hardware bitmaps and Graphics
don't. Don't run dumpsys meminfo in a loop while you measure frame timing. It
forces a garbage collection in your app and walks all of its memory mappings,
which changes what you're trying to measure.
Sweep candidate budgets
You can try candidate budgets without rebuilding your app. Use
am memory-limiter manual to set a budget for a
running process, and none to remove it:
adb shell am memory-limiter manual <PID> 120MBadb shell am memory-limiter manual <PID> none
A manual override lasts for the lifetime of that process instance, across
process state changes, so clear it or restart the process before you move on.
It also sets the process's hard limit (memory.max) to the same value, so a
candidate budget below your app's working set can get the process killed
instead of just slowing it down. Treat that as a failed budget.
You can also sweep from a benchmark build by reading a candidate value from an
instrumentation argument and setting
MemoryBudgetManager.processBudgetBytes. A runtime budget
can only tighten a manifest budget, so sweep on a build without
<memory-budget> declarations. It also stays in force across process state
changes until you call clearProcessBudget.
Start at the highest memory.current from your baseline and step down 10 to
15 MB at a time. For each candidate budget:
- Start a fresh process, drive it into the target state, and apply the budget.
- Run the user flow once as a warm-up. Applying a budget evicts cold startup pages once, and you want to measure steady state rather than that transition.
Reset frame stats, and record the starting refault counters and the process CPU time (
utimeandstime, in clock ticks):adb shell dumpsys gfxinfo <package-name> resetadb shell "grep -E '^workingset_refault_(anon|file) ' $CG/memory.stat"adb shell "cut -d ' ' -f 14,15 /proc/$PID/stat"Run the measured pass of your user flow.
Record
memory.current,anon,file, the refault counters, and CPU time again, and collect frame stats.dumpsys gfxinfocan print the same stats more than once, so take the first match, which covers the whole app:adb shell "dumpsys gfxinfo <package-name>" | grep -m 1 'Janky frames:'adb shell "dumpsys gfxinfo <package-name>" | grep -m 1 '95th percentile'
Repeat each candidate at least three times, and interleave candidates rather than running all repetitions of one candidate back to back. Compare each result with the spread of your baseline runs, not with a single run.
Read the results
As the budget drops, your app goes through three regimes:
- Above the knee:
memory.currentfollows the budget down by shedding coldfilepages and idle heap.anonstays flat, refaults stay near baseline, and CPU time and frame times don't move. This is where you want to be. - Below the knee: The cold pages are gone, so the kernel starts swapping out heap pages that your app still uses. Refaults jump by hundreds or thousands of pages per run and CPU time goes up. Frame times might barely move on an otherwise idle test device, but your app now spends CPU time and battery compressing and decompressing its own working set.
- Well below the knee: Reclaim and swap-in faults land on the UI thread
and
RenderThread, and jank, frame times, and warm resume times all go up.
Compared with your unconstrained baseline, a candidate budget passes if it meets all of the following, which are a reasonable starting point:
- The process survives the run
- Refault deltas stay close to baseline
- Process CPU time rises by no more than 5%
- Janky frames rise by no more than 1 percentage point
- P95 frame time rises by no more than 3 ms
- Warm resume time (
TotalTimefromadb shell am start -W) rises by no more than 15%, forperceptibleandbackgroundbudgets
The knee is the lowest candidate budget that passes. For example, here's a foreground sweep for a hypothetical app with a 138 MB unconstrained peak:
| Candidate budget | memory.current |
anon |
file |
Refaults (pages) | P95 frame time | Regime |
|---|---|---|---|---|---|---|
| None | 138 MB | 75 MB | 63 MB | 0 | 10.5 ms | Baseline |
| 125 MB | 121 MB | 74 MB | 47 MB | 10 | 10.5 ms | Above the knee |
| 110 MB | 110 MB | 75 MB | 35 MB | 15 | 10.8 ms | Knee |
| 95 MB | 95 MB | 74 MB | 21 MB | 176 | 11.2 ms | Below the knee |
| 85 MB | 85 MB | 68 MB | 17 MB | 2,050 | 12.8 ms | Below the knee |
| 70 MB | 70 MB | 54 MB | 16 MB | 8,771 | 16.5 ms | Well below the knee |
Pick your budget
Set android:maxMb 10% to 20% above the knee, so that differences in user
data, locales, and allocation bursts don't push your app below it. In the
preceding example, the knee is at 110 MB, so 125 MB is a good choice.
Then install a release build with the new <memory-budget> declarations, run
your user flow again, and check that the pass criteria still hold. dumpsys
meminfo shows the active budget and its source in its Memory Budget section.
Validate non-foreground budgets with a warm return
A tight perceptible or background budget can look fine while your app is in
that state and then cost you when the user comes back. Test the return trip
too:
- Run the foreground flow, move the app into the target state, and apply the
candidate budget with
am memory-limiter manual. - Let the perceptible or background work run for 10 to 15 seconds.
- Clear the override (
adb shell am memory-limiter manual <PID> none), then immediately bring the app back withadb shell am start -Wand recordTotalTime. If you set the candidate throughMemoryBudgetManagerinstead, callclearProcessBudgetbefore the app returns. - Run a short foreground interaction and check refaults and frame stats against the pass criteria.
Pick the lowest budget that passes both while the app is in that state and on the warm return, and add the same 10% to 20% headroom.
Calibrate display scaling
If you use android:additionalBytesPerDisplayPixel, find the foreground knee on
two displays with different resolutions, such as 1080 × 2400
(≈ 2.59M pixels) and 1440 × 3120 (≈ 4.49M pixels).
Divide the difference between the two knees, in bytes, by the difference in
pixel count to get android:additionalBytesPerDisplayPixel. Subtract that
per-pixel amount for the smaller display from its knee to get the
resolution-independent base, and add headroom to get android:maxMb.
Runtime APIs (secondary dynamic option)
Declaring budgets statically in AndroidManifest.xml is the preferred solution
for almost all apps. However, for applications with dynamic workloads or for
runtime experimentation, Android provides runtime SDK and NDK APIs as a
secondary option.
The runtime API lets you:
- Query current memory usage and effective budgets.
- Dynamically adjust your process budget downward.
- Listen for over-budget events to proactively trim caches before the operating system triggers direct reclaim.
Android SDK API (MemoryBudgetManager)
The MemoryBudgetManager system service is available to apps written
in Kotlin and Java starting in Android 17 QPR2 (minor SDK release, API level
37.2 / Build.VERSION_CODES_FULL.CINNAMON_BUN_2).
Retrieve the service
Before you access MemoryBudgetManager, check that the device runs
Android 17 QPR2 or higher by checking SDK_INT_FULL. If
your app's minSdk is lower than 36, check SDK_INT first so
devices running Android 15 (API level 35) or lower don't throw
NoSuchFieldError when reading SDK_INT_FULL:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA &&
Build.VERSION.SDK_INT_FULL >= Build.VERSION_CODES_FULL.CINNAMON_BUN_2
) {
val budgetManager = context.getSystemService(MemoryBudgetManager::class.java)
}
Query usage and budgets
// Query current memory charged to this process and the package UID
val processUsageBytes = budgetManager.processCurrentUsageBytes
val packageUsageBytes = budgetManager.packageCurrentUsageBytes
// Query effective budgets (returns LIMIT_IS_DISABLED if unconstrained)
val processBudgetBytes = budgetManager.processBudgetBytes
val packageBudgetBytes = budgetManager.packageBudgetBytes
Dynamically set or clear budgets
You can set a tighter budget at runtime to constrain memory during lightweight tasks, or clear it when the task finishes:
// Set a tighter dynamic budget on the current process (e.g., 96 MB)
try {
budgetManager.processBudgetBytes = 96L * 1024L * 1024L
} catch (e: IllegalArgumentException) {
// Thrown if the budget is <= 0 or exceeds the manifest ceiling or system limit
Log.e(TAG, "Requested budget exceeds manifest or system ceiling", e)
}
// Clear the dynamic process budget to restore the manifest limit (or unconstrained baseline)
budgetManager.clearProcessBudget()
Unlike <memory-budget> manifest clauses, which automatically switch budgets
whenever the process transitions between foreground, perceptible, and
background states, a dynamic budget set through MemoryBudgetManager applies
a single ceiling until your code updates or clears it. If you use the runtime
API to run field experiments or manage state-specific limits, update or clear
the dynamic budget across lifecycle transitions (for example, using
ProcessLifecycleOwner or activity lifecycle
callbacks, as shown in the Adaptive image editor example).
Performance and idempotency
When working with MemoryBudgetManager at runtime, keep the following
characteristics in mind:
- Idempotency: All budget mutation APIs are idempotent. Calling
clearProcessBudget()orclearPackageBudget()repeatedly or when no dynamic budget is active is a safe no-op. Similarly, settingsetProcessBudgetBytes()orsetPackageBudgetBytes()to the same value consecutively doesn't trigger redundant system reconfigurations. - Calling frequency: Setting, updating, or clearing a budget makes a cross-process call to the system service to update the operating system memory controller for the process. While lightweight, it isn't completely free. Call these APIs during discrete lifecycle transitions and task milestones (such as entering or leaving an activity, starting or completing background processing, or handling speech requests), and avoid calling them in tight loops, audio processing threads, or per-frame render routines.
- Query frequency: The query properties
processCurrentUsageBytesandpackageCurrentUsageBytesfetch live memory usage from the system service. While fast, queries should be invoked on demand (such as during task transitions or insideOnOverBudgetListenercallbacks) rather than polled in continuous loops.
Listen for over-budget pressure callbacks
Apps can register a listener to be notified when memory usage exceeds the budget threshold. This allows the app to perform proactive app-level cleanup (such as clearing in-memory bitmap caches) while the operating system reclaims memory to keep the process or package within its budget:
val cleanupThread = HandlerThread("MemoryBudgetCleanup").apply { start() }
val listener = MemoryBudgetManager.OnOverBudgetListener { budgetBytes ->
Log.w(TAG, "Process exceeded memory budget of $budgetBytes bytes")
// Proactively evict thread-safe caches on the background Looper
imageTileCache.evictAll()
}
// Register on a background Looper before tightening a dynamic budget
budgetManager.registerProcessOverBudgetListener(cleanupThread.looper, listener)
// When done (for example, in onStop)
budgetManager.unregisterProcessOverBudgetListener(listener)
Best practices for over-budget callbacks:
- Use a background
Looper: Pass a backgroundHandlerThreadLoopertoregisterProcessOverBudgetListenerorregisterPackageOverBudgetListenerwhenever possible. Running cache eviction or file cleanup on a background thread avoids contending with frame rendering on the UI thread while the process is under memory pressure. ReserveLooper.getMainLooperfor listeners that only touch main-thread-bound UI state. - Expect repeated callbacks: Over-budget callbacks aren't one-shot
notifications. When usage exceeds the budget, the operating system reclaims
cold pages first, and if pressure continues, the system keeps reclaiming
memory and invoking your listener at most once every second. Make your
callback idempotent. If your app only needs a single notification or
wants a longer cooldown between cleanup passes, call
unregisterProcessOverBudgetListener(orunregisterPackageOverBudgetListener) inside the callback, or guard your cleanup with a cooldown timestamp orAtomicBoolean. - Register before tightening a budget: If current memory usage already
exceeds a new budget when you call
setProcessBudgetBytesorsetPackageBudgetBytes, an over-budget event is triggered as soon as the new budget is applied. Register your listener before setting the budget so your app doesn't miss that initial over-budget notification. - Be quick: Reclaim operations must offer immediate relief. Complex computations during pressure worsen performance.
- Avoid allocations: Don't allocate new objects or start new threads inside the callback, as doing so can trigger immediate operating system direct reclaim.
- Focus on high-yield targets: Evicting large Bitmaps, render buffers, or closing memory-mapped files is much more effective than releasing many small objects.
Native NDK API (<android/memory_budget_manager.h>)
Native apps can use the C NDK API exposed by libandroid.so starting in
Android 17 QPR2 (API level 37.2).
CMake configuration
find_library(android-lib android)
target_link_libraries(my_native_engine PRIVATE ${android-lib})
Include header and query usage
#include <android/memory_budget_manager.h>
// Query current memory usage
int64_t process_usage = AMemoryBudgetManager_getProcessCurrentUsageBytes();
int64_t package_usage = AMemoryBudgetManager_getPackageCurrentUsageBytes();
// Query current budget
int64_t process_budget = 0;
AMemoryBudgetResult result = AMemoryBudgetManager_getProcessBudget(&process_budget);
if (result == AMEMORY_BUDGET_RESULT_SUCCESS) {
// Current budget available in process_budget
} else if (result == AMEMORY_BUDGET_RESULT_LIMIT_IS_DISABLED) {
// No budget is currently active
}
Dynamically configure native budget
// Set a tighter process budget (e.g. 160MB)
AMemoryBudgetResult result = AMemoryBudgetManager_setProcessBudget(160LL * 1024 * 1024);
if (result != AMEMORY_BUDGET_RESULT_SUCCESS) {
const char* error_msg = AMemoryBudgetManager_resultToString(result);
// Handle error (e.g. AMEMORY_BUDGET_RESULT_ERROR_EXCEEDS_MANIFEST_LIMIT)
}
// Clear the dynamic budget to resume manifest limits (or unconstrained baseline)
AMemoryBudgetManager_clearProcessBudget();
Monitor memory pressure events
The NDK provides two ways to monitor memory events:
- High-Level Watcher (
AMemoryBudgetManager_Watcher_create): Monitors events on anALooperwith automatic debouncing. - Low-Level File Descriptor:
AMemoryBudgetManager_getProcessMemoryPressureFdreturns a native file descriptor that can be integrated directly into a customepollengine loop.
void onMemoryPressure(int32_t event_mask, const AMemoryBudgetEvents* events, void* userdata) {
// High-yield eviction of unused native textures or geometry caches
purgeNativeTextureCaches();
}
// Register watcher on a background worker ALooper with a 1000ms debounce interval
AMemoryBudgetManagerWatcher* watcher = AMemoryBudgetManager_Watcher_create(
background_looper,
AMEMORY_BUDGET_MANAGER_EVENT_PROCESS,
1000 /* debounce_ms */,
&onMemoryPressure,
NULL /* userdata */
);
// When done:
AMemoryBudgetManager_Watcher_destroy(watcher);
Bind the watcher to a background worker thread's ALooper, rather than your
main UI or render thread, so that cache eviction doesn't contend with frame
rendering. Create the watcher before tightening a budget with
AMemoryBudgetManager_setProcessBudget or
AMemoryBudgetManager_setPackageBudget so your app doesn't miss the initial
over-budget event.
While the process or package remains over budget and the operating system
continues reclaiming memory, the watcher invokes the callback repeatedly (at
most once per debounce_ms interval). Make your callback idempotent, or call
AMemoryBudgetManager_Watcher_destroy (which is safe to call from inside the
callback) if your app only needs a one-shot notification.
Runtime API examples
The following examples demonstrate how to implement the runtime APIs using the Android SDK API (written in Kotlin) and the Native NDK API (written in C++).
Android SDK example: Adaptive image editor
This example shows an image editing app (com.example.imageeditor) whose
manifest declares a 256MB ceiling to accommodate its multi-layer canvas:
<manifest ... >
<application ... >
<!-- Manifest ceiling accommodates the heaviest editing workload -->
<memory-budget android:maxMb="256" />
</application>
</manifest>
When the user is browsing the lightweight thumbnail gallery, the app uses the
Android SDK API in Kotlin to dynamically tighten its process budget to 96MB.
When the user opens the multi-layer editing canvas, the app clears the dynamic
budget to restore the full 256MB manifest ceiling. It also registers an
OnOverBudgetListener on a background HandlerThread
Looper before applying the gallery budget to evict cached preview bitmaps
under pressure without blocking the UI thread.
package com.example.imageeditor.ui
import android.app.Activity
import android.app.MemoryBudgetManager
import android.graphics.Bitmap
import android.os.Bundle
import android.os.HandlerThread
import android.util.Log
import android.util.LruCache
class ImageEditorActivity : Activity() {
private lateinit var budgetManager: MemoryBudgetManager
private lateinit var cleanupThread: HandlerThread
// Thread-safe in-memory cache for rendered preview tiles (32MB limit)
private val previewCache = object : LruCache<String, Bitmap>(32 * 1024 * 1024) {
override fun sizeOf(key: String, value: Bitmap): Int = value.byteCount
}
private val overBudgetListener = MemoryBudgetManager.OnOverBudgetListener { budgetBytes ->
Log.w(TAG, "Process memory pressure detected (budget: ${budgetBytes / 1048576}MB). Evicting preview cache.")
previewCache.evictAll()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
budgetManager = getSystemService(MemoryBudgetManager::class.java)
cleanupThread = HandlerThread("MemoryBudgetCleanup").apply { start() }
}
override fun onStart() {
super.onStart()
// Register listener on a background Looper before tightening the budget
budgetManager.registerProcessOverBudgetListener(cleanupThread.looper, overBudgetListener)
// Constrain memory during lightweight gallery browsing
applyGalleryBudget()
}
override fun onStop() {
super.onStop()
budgetManager.unregisterProcessOverBudgetListener(overBudgetListener)
}
override fun onDestroy() {
cleanupThread.quitSafely()
super.onDestroy()
}
/**
* Called when the user enters the high-resolution editing canvas.
* Clears the dynamic budget, restoring the full 256MB manifest ceiling.
*/
fun enterEditingCanvas() {
// Clear the tighter dynamic budget to restore the full manifest ceiling (256MB)
budgetManager.clearProcessBudget()
Log.i(TAG, "Restored manifest budget ceiling (256MB) for editing canvas")
}
/**
* Called when the user exits the editor back to the thumbnail gallery.
* Re-applies the tighter dynamic budget.
*/
fun exitToGallery() {
previewCache.trimToSize(8 * 1024 * 1024)
applyGalleryBudget()
}
private fun applyGalleryBudget() {
try {
// Dynamically tighten budget to 96MB for the lightweight gallery view
budgetManager.processBudgetBytes = 96L * 1024L * 1024L
Log.i(TAG, "Tighter dynamic budget applied for gallery: 96MB")
} catch (e: IllegalArgumentException) {
Log.e(TAG, "Could not apply dynamic budget", e)
}
}
companion object {
private const val TAG = "ImageEditor"
}
}
NDK C++ example: Native 3D engine
This example shows a native C++ game engine managing memory budgets based on the
active graphics quality level, assuming the application's manifest declares a
512MB ceiling to accommodate High quality graphics (android:maxMb="512"). The
engine starts AMemoryBudgetManager_Watcher_create on a background worker
thread's ALooper before tightening the budget for lower quality presets, and
unloads texture mipmaps when over budget.
#include <android/memory_budget_manager.h>
#include <android/looper.h>
#include <android/log.h>
#define LOG_TAG "Native3DEngineMemory"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
class MemoryGovernor {
public:
MemoryGovernor() : mWatcher(nullptr) {}
~MemoryGovernor() {
stopMonitoring();
}
// Configures process budget based on user graphics quality settings
bool setQualityBudget(int qualityLevel) {
int64_t targetBytes = 0;
switch (qualityLevel) {
case 0: // Low (budget: 128MB)
targetBytes = 128LL * 1024 * 1024;
break;
case 1: // Medium (budget: 256MB)
targetBytes = 256LL * 1024 * 1024;
break;
case 2: // High (budget: 512MB)
targetBytes = 512LL * 1024 * 1024;
break;
default:
// Clear dynamic override and restore manifest limit
AMemoryBudgetManager_clearProcessBudget();
return true;
}
AMemoryBudgetResult result = AMemoryBudgetManager_setProcessBudget(targetBytes);
if (result != AMEMORY_BUDGET_RESULT_SUCCESS) {
LOGW("Could not set quality budget: %s", AMemoryBudgetManager_resultToString(result));
return false;
}
return true;
}
// Call on a background worker ALooper before tightening the budget with setQualityBudget
bool startMonitoring(ALooper* backgroundLooper) {
if (!backgroundLooper) return false;
// Monitor process budget events, debounced to at most once every 1000ms
mWatcher = AMemoryBudgetManager_Watcher_create(
backgroundLooper,
AMEMORY_BUDGET_MANAGER_EVENT_PROCESS,
1000,
&MemoryGovernor::onPressureEvent,
this
);
return mWatcher != nullptr;
}
void stopMonitoring() {
if (mWatcher) {
AMemoryBudgetManager_Watcher_destroy(mWatcher);
mWatcher = nullptr;
}
}
void unloadUnusedTextures() {
LOGW("Memory pressure callback triggered. Purging cached texture mipmaps...");
// Fast, idempotent, high-yield eviction without allocating memory
}
private:
static void onPressureEvent(
int32_t event_mask,
const AMemoryBudgetEvents* events,
void* userdata
) {
auto* governor = static_cast<MemoryGovernor*>(userdata);
governor->unloadUnusedTextures();
}
AMemoryBudgetManagerWatcher* mWatcher;
};