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:

  1. 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.
  2. 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.
  3. 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.
  4. 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 high oom_score_adj and 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 registers ComponentCallbacks2 listeners to receive OS memory pressure signals. When Android sends a TRIM_MEMORY_RUNNING_LOW or TRIM_MEMORY_BACKGROUND signal, Chromium immediately purges internal caches and strips any lingering moderate bindings from background renderers, offering them up to lmkd for 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.