How to Declare App Memory Budgets in Android 17
Learn to declare Android 17 app memory budgets in the manifest with state-specific, multi-process, and runtime options to control RAM use.
Android 17 introduces per-app memory limits to keep one heavy app from slowing the whole device. You can go further by declaring your own memory budgets. These tell the system your expected working set so it can reclaim unused pages with eviction and zRAM swap before you hit the harder system ceiling.
Budgets take effect on Android 17 QPR2 (API level 37.2) and higher. On older versions the platform ignores the new XML elements, so you can ship them safely. Exceeding a budget does not kill your process. The OS first drops cold file-backed pages, then compresses anonymous heap into zRAM. As long as your active working set stays under the budget, performance stays normal. Set the budget too low and you pay in extra CPU and possible jank from refaults.
This guide shows how to declare budgets in the manifest, calibrate them against real user flows, and use the runtime APIs when workloads change dynamically. It builds on the system-level limits covered in our Android 17 per-app memory limits guide.
Declare a baseline budget
The simplest approach puts one <memory-budget> inside the <application> tag. It applies across all processes and states.
<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 256 MB resident memory ceiling. When the process exceeds it, the kernel starts reclaiming inactive pages. Size the number for your typical working set, not the absolute peak RSS you see after a long session.
Vary budgets by process state
Apps need different amounts of memory depending on visibility. Declare multiple clauses with the android:state attribute.
<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 clause without android:state acts as the fallback for unspecified states (usually foreground). More specific clauses override it when the process moves to perceptible or background.
The platform maps these to RunningAppProcessInfo.importance:
- foreground:
IMPORTANCE_FOREGROUNDorIMPORTANCE_TOP_SLEEPING(visible Activity or top app). - perceptible:
IMPORTANCE_FOREGROUND_SERVICEorIMPORTANCE_VISIBLE(media playback, navigation, active downloads). - background: lower importance levels for jobs, receivers, and syncs.
When the process becomes cached (IMPORTANCE_CACHED), the platform releases the active budget and falls back to normal cached-process reclamation.
Multi-process budgets
If you split work across processes, place <memory-budget> inside a <process> tag under <processes>.
<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>
</application>
</manifest>
Memory charged to a subprocess counts against both its own process budget and the package budget. The tighter of the two applies. List only the processes that need a custom budget; others stay under the system limit.
Scale for display density and form factors
For apps whose memory grows with screen pixels, use the optional scaling attributes.
<!-- Baseline 180MB + 16MB multiplied by (density ratio)^2 -->
<memory-budget
android:maxMb="180"
android:additionalMbPerDensity="16"/>
On an xxhdpi display this grows the budget significantly. Alternatively, android:additionalBytesPerDisplayPixel adds a fixed number of bytes per physical pixel—useful for full-screen buffers.
For Wear OS or other form factors, add android:feature="watch" (or automotive, leanback) on a tighter clause. The last applicable clause in the manifest wins, so put the general baseline first and the specialized overrides after it.
Measure working set and calibrate
A good budget sits just above the knee—the lowest value where reclaim only drops cold slack and refaults stay near baseline. Below the knee the kernel swaps out pages the app still needs, raising CPU time and eventually frame times.
Test on a release build (R8-optimized, non-debuggable) running Android 17 QPR2. Script a 30–60 second user flow for each state with Macrobenchmark or UI Automator. Start each run from a fresh process (adb shell am force-stop).
Sample the cgroup files:
PID=$(adb shell pidof -s com.example.app)
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"
You can also sweep candidate budgets without rebuilding:
adb shell am memory-limiter manual <PID> 120MB
adb shell am memory-limiter manual <PID> none
A candidate passes if the process survives, refaults stay close to baseline, CPU time rises ≤5 %, janky frames rise ≤1 percentage point, and P95 frame time rises ≤3 ms. Set android:maxMb 10–20 % above the knee so normal variation does not push you below it.
Runtime APIs for dynamic control
Manifest declarations are preferred. For workloads that change (for example an image editor that is light while browsing thumbnails and heavy while editing), use MemoryBudgetManager on API 37.2+.
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)
try {
budgetManager.processBudgetBytes = 96L * 1024L * 1024L
} catch (e: IllegalArgumentException) {
// Exceeds manifest ceiling or system limit
}
// Later: budgetManager.clearProcessBudget()
}
A runtime budget can only tighten a manifest ceiling; it cannot raise it. Register an OnOverBudgetListener on a background HandlerThread so you can drop large caches before the OS does more aggressive reclaim. Call the APIs only at lifecycle or task boundaries, not in tight loops.
Native code can use the NDK headers in libandroid.so (AMemoryBudgetManager_setProcessBudget and related functions).
Testing and verification
After adding the declarations, install a release build and check adb shell dumpsys meminfo <package>. The Memory Budget section shows the active budget and its source. Also inspect process state with adb shell dumpsys activity processes <package>.
Watch for the system memory limiter as well. If your app still hits the device-wide ceiling, you will see exit reasons containing “MemoryLimiter:AnonSwap”. The budgets you declare help you stay well under that hard limit.
Tips for production
- Prefer a single baseline budget unless you have clear state-specific or multi-process needs.
- Calibrate on the exact release build and device classes your users have.
- Keep 10–20 % headroom above the measured knee.
- Use runtime APIs only for temporary tightening; clear them when the heavy work ends.
- Combine with R8 shrinking and bitmap best practices. Less resident code and fewer large bitmaps make every budget easier to meet.
Conclusion
Declaring <memory-budget> elements gives you an explicit contract with the Android 17 memory system. The OS reclaims cold pages and swaps inactive heap so your process stays within the working set you measured, without the abrupt kills that come from the system-wide limiter. Start with a simple baseline, measure the knee on real flows, then add state or process overrides only where they reduce field memory use. The runtime APIs remain available when a single static value cannot cover every mode of your app.
Official documentation: Set app memory budgets.
Share this story