Chromium on Android: OS Process Limits and OOM Priorities
This article provides an overview of how the Chromium browser on Android navigates the operating system's strict memory management rules and background process limits. Unlike desktop environments, Android aggressively terminates background tasks to preserve system resources for foreground applications. To maintain its secure multi-process architecture without crashing the entire browser, Chromium uses pre-declared Android services, dynamic service binding flags, and system memory pressure listeners to actively align with the Android Out-Of-Memory (OOM) killer priorities.
Pre-Declared Services Instead of Forking
On desktop platforms, Chromium creates new processes for tabs
(renderer processes), extensions, and the GPU by directly asking the OS
kernel to fork a process. Android restricts standard applications from
doing this arbitrarily. Instead, Chromium must declare its child
processes as distinct <service> entries in its
AndroidManifest.xml file, typically named
SandboxedProcessService0 through
SandboxedProcessService39.
Because these services are statically declared in the manifest,
Chromium has a hard ceiling on how many isolated renderer processes it
can run simultaneously (usually 40). Furthermore, these services use the
android:isolatedProcess="true" flag, which runs them under
a unique, unprivileged user ID with no permissions of their own,
satisfying Chromium's strict sandboxing requirements while playing by
Android's application framework rules.
Controlling OOM Priorities via Service Bindings
Android uses a Low Memory Killer Daemon (lmkd) to
terminate processes when system RAM runs low. Every process is assigned
an adjustment score (oom_score_adj); higher scores mean the
process is more likely to be killed. Android apps cannot manually write
to this score. Instead, the Android system calculates it based on the
state of the application's components.
Chromium's main browser process manipulates the OOM scores of its
child renderer processes by holding different types of Android service
bindings (bindService()) to them. Chromium manages four
primary binding levels for each child process:
- Strong Binding
(
Context.BIND_IMPORTANT): Gives the renderer process the same high priority as the main foreground browser process. Chromium applies this to the currently visible, active tab. The OS will almost never kill this process unless the entire system is critically out of memory. - Visible/Moderate Binding
(
Context.BIND_NOT_FOREGROUND): Tells Android the process is doing work perceptible to the user, such as a background tab playing audio or a visible subframe. It sits below the active app in priority, but above standard cached background apps. - Perceptible Binding
(
Context.BIND_NOT_PERCEPTIBLE): Used on newer Android versions to finely tune processes that are doing background work the user might care about, giving them slightly better protection than pure cached processes. - Waived Binding
(
Context.BIND_WAIVE_PRIORITY): Every child process has a waived binding just to keep the communication channel open. When a tab is moved to the background and is no longer active, Chromium drops the Strong and Moderate bindings, leaving only the Waived binding. This drops the renderer process into Android's "cached" LRU (Least Recently Used) list, giving it a highoom_score_adjand signaling to the OS that it is completely safe to kill.
Managing Strict Background Process Limits
Android enforces strict limits on how many cached processes can exist across the entire system at once. If Chromium kept strong bindings to all open tabs, Android would be unable to reclaim memory, eventually forcing the OS to kill the main Chromium browser process itself—resulting in a full browser crash.
To prevent this, Chromium's internal BindingManager and
ChildProcessLauncher constantly monitor tab visibility:
- Immediate Downgrading: The moment a user switches from Tab A to Tab B, Chromium applies a Strong binding to Tab B's renderer and removes the Strong binding from Tab A's renderer (often after a brief delay of a few seconds to prevent thrashing during quick tab switching).
- Responding to
onTrimMemory: Chromium registersComponentCallbacks2listeners to receive OS memory pressure signals. When Android sends aTRIM_MEMORY_RUNNING_LOWorTRIM_MEMORY_BACKGROUNDsignal, Chromium immediately purges internal caches and strips any lingering moderate bindings from background renderers, offering them up tolmkdfor immediate termination. - Process Sharing: If a user opens so many tabs that Chromium reaches its manifest limit of 40 sandboxed services, or if the OS rejects new service creation due to system-wide process limits, Chromium falls back to sharing existing renderer processes among multiple tabs from the same site (Site Isolation permitting).
Graceful Recovery from OOM Termination
When lmkd kills a background renderer process that only
held a Waived binding, the main Chromium browser process remains
completely unaffected.
The main browser process detects the termination because the Android
Binder IPC channel (onServiceDisconnected) closes
unexpectedly. Because Chromium already knew the tab was in the
background, it does not show an error to the user. It simply keeps the
tab's title, URL, and scroll state in a lightweight metadata object in
the main browser process. When the user eventually taps on that
background tab again, Chromium spawns a new
SandboxedProcessService, applies a Strong binding to it,
and seamlessly reloads the webpage's state.