Android · Process lifecycle

Who killed my process? An Android whodunit

Your app was fine a minute ago and now it is back with amnesia. Everyone who can end an Android process, how the system ranks its victims, how to read the evidence afterwards, and how to write an app that shrugs it off.

Updated 2026-09-10Kotlin · Jetpack Compose
On this page

The bug report is always the same. "I switched to the camera, came back, and the app had forgotten everything." No crash dialog, no stack trace, nothing in Crashlytics. The user swears they did nothing. They are telling the truth: somebody else ended the process while they were away, and Android quietly rebuilt the screen from notes so that the return would look seamless. This article is the investigation. Who has a motive, who has the means, what evidence each one leaves behind, and how to build an app that treats the whole affair as routine.

The map

Read this first when short on time. Every branch is a section below.

Figure 1. The whole investigation on one page. Every branch is a section below; fold what you know, open what you do not.

The crime scene: an app that came back with amnesia

Process death is the system ending an app's process while the user is elsewhere, then recreating the app's screens from saved notes when the user returns, so the app looks like it never left while every object in memory is gone.

The tell-tale symptoms are all in the shape of "the screen is right but the data is wrong". A detail screen shows with an empty title because the object it expected in a singleton is null. A lateinit property throws because the Application that set it up is a different Application instance now. A multi-step form is back on step three with steps one and two blank. A repository's in-memory cache is cold, so a screen that never fetched on its own has nothing to show. Nothing in a crash reporter, because nothing crashed: the process was killed with a signal, not an exception, and the recreated process is a fresh, healthy one.

sequenceDiagram
  autonumber
  participant U as User
  participant S as system_server
  participant P1 as Process, pid 4120
  participant P2 as Process, pid 9877
  U->>P1: presses Home
  P1->>S: onSaveInstanceState Bundle travels with onStop
  S->>P1: minutes later: SIGKILL (memory, swipe, policy)
  Note over P1: everything in memory is gone
  U->>S: taps the app in Recents
  S->>P2: fork from Zygote, bindApplication, Application.onCreate
  S->>P2: launch the top activity with the saved Bundle
  P2-->>U: the same screen, rebuilt from the Bundle
Figure 2. The user sees steps 1, 4 and 7 and calls it "resuming". Between them a process died and a different one was born; the only bridge is the Bundle held by system_server.

It is worth being precise about what this is not. A crash is the process ending from inside, with an exception; the user sees the app vanish or a dialog, and crash reporters record it. A configuration change, such as rotation, destroys and recreates the Activity inside the same process, so ViewModels, singletons and the Application all survive. Process death sits between them: nothing in your code went wrong, yet only what was written down survives.

Note

The recreated process has a new pid, Application.onCreate runs again, every content provider initialises again, and dependency injection graphs are rebuilt from scratch. The startup article covers that sequence; it is a warm start with a saved Bundle, and it is exactly the same code path as a cold start except for what the top Activity receives in onCreate.

In the wild
  • Chrome restores every open tab after its process is killed because tab state is written to disk continuously; the browser treats being killed as normal, which is the attitude this article recommends.
  • Firebase Crashlytics and similar tools never report a memory kill, because it is not an exception in the app; since Android 11 they do report ANRs by reading the exit records covered below.
  • Android Studio's stop button force-stops the app, which removes its task as well, so pressing it and relaunching does not reproduce this bug. The recipe that does is at the end.
Process death
The system killing an app's process while the user is not looking at it, followed by recreation of the task and top Activity from saved state when the user returns.
Saved instance state
The Bundle an Activity fills in onSaveInstanceState, kept by system_server and handed back to onCreate after recreation.

The suspects, at a glance

Every process death on Android has a recorded reason, and the list of reasons is the list of suspects: a memory daemon, two kinds of user action, a vendor's power manager, and half a dozen system policies.

Since Android 11 the system keeps a short history of why each of an app's processes ended, exposed as ApplicationExitInfo. Its reason codes are the cleanest catalogue of suspects there is, so this table is the whole cast; each row gets its own section below.

SuspectReason codeMotiveTypical description string
lmkd, the low memory killerREASON_LOW_MEMORYMemory pressure; your process was the least important one big enough to helpnone; the kill is logged by lowmemorykiller instead
The user, swiping in RecentsREASON_USER_REQUESTED"I am done with this"remove task
The user, pressing Force stopREASON_USER_STOPPED"Make it stop"; also what many OEM battery managers do on your behalfstop com.example
The system, revoking a permissionREASON_PERMISSION_CHANGEA runtime permission changed; the process must restart to see the new statepermissions revoked
The system, installing an updateREASON_PACKAGE_UPDATEDThe code on disk changedreplace pkg
The system, after an ANRREASON_ANRThe main thread stopped answeringuser request after error
Yourself, crashingREASON_CRASH, REASON_CRASH_NATIVEAn uncaught exception or a native signalcrash
Yourself, exitingREASON_EXIT_SELF, REASON_SIGNALEDSystem.exit, Process.killProcess, or a signal from outsideexit code or signal number
A neighbourREASON_DEPENDENCY_DIEDA content provider you held a stable reference to dieddepends on provider ...
The freezerREASON_FREEZERYour cached process was frozen and somebody made a synchronous Binder call into itSync transaction while in frozen state
Resource policeREASON_EXCESSIVE_RESOURCE_USAGEToo much CPU in the background, or too many child processesexcessive cpu ... phantom process
SetupREASON_INITIALIZATION_FAILUREThe process could not even get to Application.onCreatebind timeout, missing class

Two suspects in that list account for almost every "amnesia" report: the low memory killer and the user's own swipe. The rest matter because each one behaves slightly differently afterwards, and the differences decide whether your alarms still fire, whether push messages still arrive, and whether the next launch gets a saved Bundle at all.

In the wild
  • Sentry and Crashlytics read this exact list on the next launch to attach "why the last process ended" to their reports; ANRs in both tools are built on REASON_ANR and its trace.
  • Termux users met REASON_EXCESSIVE_RESOURCE_USAGE in numbers when Android 12 began killing "phantom" child processes; the reason codes were how the community worked out what was happening.

Suspect one: lmkd and the memory market

The low memory killer is a user-space daemon that watches memory pressure and, when it rises, kills the process with the worst importance score, chosen from a ladder that ActivityManager maintains for every process on the device.

Android phones have no swap partition to speak of. Some compress pages into a zram block in RAM, but when memory runs out the only real lever is ending a process. Linux has its own OOM killer for that, but it fires only when allocation has already failed, which is far too late for a phone that must keep the foreground app smooth. Android therefore kills early and on purpose. Until Android 8 the killing was done inside the kernel by the lowmemorykiller driver, with a small daemon feeding it thresholds; since Android 9 that daemon, lmkd, does the killing itself, reading the kernel's pressure stall information (PSI) on Android 10 and later, and older vmpressure events before that. The kernel OOM killer remains as the last resort and uses the same scores.

The ladder

Every process has an oom_score_adj in /proc/pid/oom_score_adj, from -1000 (never kill) to 999 (kill first). ActivityManager recomputes each app's score whenever anything changes, sends it to lmkd over a socket, and lmkd writes it into /proc. The rungs are roughly these, and they shift a little between releases:

flowchart TD
  A["minus 1000 to minus 700<br/>native daemons, system_server, persistent apps"] --> B["0 foreground<br/>the activity the user sees, or its foreground service"]
  B --> C["100 visible<br/>an activity still partly on screen"]
  C --> D["200 perceptible<br/>music playback, a bound service the user notices"]
  D --> E["500 service<br/>a started service, no UI"]
  E --> F["600 home<br/>the launcher"]
  F --> G["700 previous<br/>the app the user was in just before this one"]
  G --> H["900 to 999 cached<br/>nothing running, kept only because RAM was free"]
Figure 3. The oom_score_adj ladder, top to bottom is safest to most exposed. lmkd works from the bottom up; among cached processes it usually takes the largest first, because one big kill frees more than three small ones.

Three rules make the ladder predictable:

  1. A process is ranked by its best component. If it has a visible Activity it is at 100 even if it also has ten idle services. A bound service borrows its client's rank: while a foreground app is bound to your service, you sit near 0 too, which the IPC article describes from the other side.
  2. Leaving the screen is a long fall. The moment the user presses Home your app goes from 0 to 700, and after the next app switch to the cached band at 900 or worse. Nothing you do in memory changes that; only holding a legitimate component does.
  3. The cached band is an LRU list. The system keeps a bounded number of cached processes, ordered by when the user last used them, and their scores climb from 900 towards 999 as they age. Being cached is not being safe; it is being next.

When PSI reports that tasks are stalling on memory, lmkd picks the process with the highest score, kills it with SIGKILL, waits for the memory to come back, and repeats if the pressure has not eased. Thresholds come from the ro.lmk.* properties and are tuned per device: a phone with 4 GB kills far earlier than one with 12 GB, which is why the bug reproduces on the tester's old device and never on the developer's new one.

# The ladder as ActivityManager sees it: proc state, oom_adj, and why
adb shell dumpsys activity processes | grep -A3 "com.example.app"
#   Proc # 4: cch+5  T/ /CAC  trm: 0 4120:com.example.app/u0a231 (cch-act)
#   oom: max=1001 curRaw=920 setRaw=920 cur=920 set=920

# The kill itself, from the daemon
adb logcat -s lowmemorykiller
#   Kill 'com.example.app' (4120), uid 10231, oom_score_adj 920 to free 184372kB rss, ... reason: low watermark is breached

# And ActivityManager's own line for any kill it ordered
adb logcat -s ActivityManager | grep Killing
#   Killing 4120:com.example.app/u0a231 (adj 920): remove task
In the wild
  • Pixel phones run zram, compressing cold pages in RAM; it delays the killer but cannot replace it, and a swap-heavy device feels slow before it starts killing.
  • Android Go devices with 1 or 2 GB set aggressive thresholds and cap cached processes hard, so on them "the user switched to the camera" is almost always followed by a kill.
  • Chrome plays the same game one level down: it kills its own background tab renderers under pressure and shows "Aw, snap" or a reload when you come back, a miniature of what the platform does to Chrome itself.
Watch out

The killer does not care how carefully you wrote your code. A leak-free app at 900 dies before a leaky app at 200. The only way to be less exposed is to hold a component the system ranks higher, and every such component is a promise to the user that something is happening. The section on staying alive lists the honest ones.

lmkd
The low memory killer daemon. Watches memory pressure and kills the process with the highest oom_score_adj until pressure eases.
oom_score_adj
A per-process integer from -1000 to 999 that both the kernel OOM killer and lmkd read to choose victims. ActivityManager sets it.
PSI
Pressure stall information: kernel counters of how long tasks waited for memory, CPU or IO. lmkd triggers on the memory one.
Cached process
A process with no active component, kept alive only in case the user comes back; the first to be killed.

The other suspects: users, vendors and the system itself

Everyone else who ends a process does so for a reason other than memory, and each leaves the app in a slightly different state afterwards, which is why the reason code matters more than the fact of the kill.

The user swipes the app away

Swiping a task out of Recents removes the task and, in stock Android, kills the process behind it unless that process is also running a foreground service. It is a polite kill: manifest receivers, alarms, scheduled jobs and push registrations all keep working, and Service.onTaskRemoved fires first so a service can tidy up. What the user loses is the task itself, so the next launch is a cold start with no saved Bundle: there is no screen to restore because the user said they did not want one.

The user presses Force stop

Force stop in Settings is the strong version. The process dies, the task goes, and the app is put in the stopped state: no manifest receiver fires, no alarm or job runs, no push message is delivered, until the user opens the app again by hand. Android treats it as "this app does nothing until I say so", and nothing in the app can undo it. This is also what many vendor power managers do to apps they judge idle, which is why a "battery saver" on one brand of phone can break alarms and messaging that work everywhere else.

The system's own reasons

stateDiagram-v2
  direction TB
  Foreground --> Cached: user leaves
  Cached --> Frozen: idle for a while
  Frozen --> Cached: work arrives, thawed first
  Cached --> Foreground: user returns
  Frozen --> Foreground: user returns
  Cached --> Killed: lmkd, swipe, policy
  Frozen --> Killed: lmkd, or a sync Binder call
  Killed --> Foreground: recreated from saved state
Figure 4. A background process is either cached, frozen or gone. Frozen is cheap for the device and invisible to the app, until someone calls into it synchronously.
After this kill...Task and saved stateAlarms and jobsManifest receivers and pushNext launch
lmkd, freezer, permission changeKeptKeptKeptRestored to the same screen with a Bundle
Swipe from RecentsRemovedKeptKeptCold start at the launcher activity
Force stop, vendor sleeperRemovedCancelledBlocked until the user opens the appCold start, and nothing ran in between
Crash or ANRTask kept, top activity's state usually lostKeptKeptOften the previous activity in the task
Package updateRemovedKeptKept, plus MY_PACKAGE_REPLACEDCold start
In the wild
  • Xiaomi, Huawei, OnePlus and Samsung have all shipped background managers that force-stop or restrict apps they consider idle; alarm clocks, fitness trackers and messengers are the usual casualties and their support pages carry per-brand instructions for the user.
  • Termux runs shells and servers as child processes, which is exactly what the Android 12 phantom process limit targets; it now warns users to disable the limit through developer settings.
  • WhatsApp and Signal re-register their work after MY_PACKAGE_REPLACED and BOOT_COMPLETED because an update or a reboot is a normal Tuesday for a messaging app.
Watch out

Do not confuse "the app was killed" with "the user removed the task". A memory kill leaves the task and its saved state in place and the next launch restores your screen; a swipe removes both and the next launch is cold. Handle both, and read the exit reason before blaming the system for a user's decision.

Stopped state
The condition after a force stop or a fresh install: the app receives no broadcasts, alarms or jobs until the user launches it explicitly.
Cached app freezer
The mechanism, default since Android 12, that puts cached processes into a cgroup freezer so they use no CPU; they are thawed before receiving work.
Phantom process
A process forked by an app outside the framework, such as a shell command; Android 12 caps them at 32 per device and kills the CPU-hungry ones.

What survives and what does not

After a kill the only things left are what was written somewhere outside the process: the system's own records about the app, and whatever the app put on disk.

ThingConfiguration changeProcess deathWhy
Activity, Fragment, View instancesRecreatedRecreatedBoth paths destroy the Activity; the difference is what the new one can find.
ViewModelSurvivesGoneHeld in the Activity's NonConfigurationInstances, which live in the process.
SavedStateHandle inside a ViewModelSurvivesSurvivesIts contents are written into the Activity's saved Bundle.
remember { } in ComposeGoneGoneLives in the composition, which is rebuilt with the Activity.
rememberSaveable { }SurvivesSurvivesBacked by the saved state registry, hence the Bundle.
Application fields, object singletons, static caches, DI singletonsSurviveGoneThey are heap memory; the heap is gone.
Context-registered receivers, in-memory listenersDepends on the ownerGoneRegistrations live in the process.
The task, back stack, each activity's Intent and saved BundleKeptKept (unless the task was removed)Stored in system_server's ActivityRecords.
Files, Room, DataStore, SharedPreferencesKeptKeptOn disk.
Alarms, JobScheduler jobs, WorkManager workKeptKept (unless force-stopped)Registered in system_server; WorkManager also persists to its own database.
Notifications already postedKeptKeptThey live in SystemUI; their PendingIntents still point at your components.
Bound service connectionsKeptDropped, then re-established by the system for BIND_AUTO_CREATE clientsThe client gets onServiceDisconnected and later a fresh binder.
flowchart TD
  A([Activity destroyed]) --> Q{Same process?}
  Q -- "Yes: rotation, locale, resize" --> R["New Activity in the old process"]
  R --> R1["ViewModel handed back<br/>singletons intact<br/>Bundle also delivered"]
  Q -- "No: the process was killed" --> D["New process, then a new Activity"]
  D --> D1["Application.onCreate again<br/>ViewModel rebuilt from scratch<br/>only the Bundle and disk remain"]
Figure 5. The two ways an Activity comes back. Code that only tests rotation exercises the left path and never learns that the right one exists.

The middle rows of the table are where bugs live. A ViewModel feels permanent because it survives rotation, so teams put the shopping cart in it and never write it down. A Hilt or Koin singleton feels even more permanent, so a "session" object gets set at login and read everywhere; after a kill it is a fresh, empty singleton and the first screen that reads it crashes. Compose's remember is the same trap one level down: it is a cache for the current composition, not storage.

In the wild
  • Jetpack Navigation saves its whole back stack, including each destination's arguments, into the saved state, so a deep screen comes back on the right destination after a kill; it is the reason arguments should be ids, not objects.
  • WorkManager keeps its queue in a Room database precisely because the process that enqueued the work may not be the one that runs it.
  • Room and DataStore are the tier of state that survives everything short of an uninstall; a repository that reads from them on demand recovers for free.
NonConfigurationInstances
An object the old Activity hands to the new one across a configuration change, inside the same process; the ViewModelStore travels in it.
SavedStateHandle
A key-value map given to a ViewModel whose contents are saved into the Activity's Bundle and restored after process death.
Saved state registry
The AndroidX mechanism that lets components register providers of saved state; rememberSaveable and Navigation both plug into it.

The system's own notebook: saved instance state

Saved instance state is a small Bundle the Activity fills when it leaves the screen, which system_server keeps alongside the task and hands back to the recreated Activity, so that the screen can be rebuilt without the process that drew it.

  1. When it is written. onSaveInstanceState runs whenever the Activity may be destroyed and later restored. Since API 28 it runs right after onStop; on older releases it ran just before it. It does not run when the user finishes the Activity with Back, because there is nothing to come back to.
  2. Where it goes. The Bundle is parcelled and sent to system_server with the stop transaction, where it sits in the ActivityRecord for that Activity. It is not written to disk: a reboot clears it, unless the Activity opted into persistableMode and used the PersistableBundle overload. The record of the task itself, with its root intent, is persisted so Recents survives a reboot.
  3. How big it may be. It travels over Binder, so it draws on the same 1 MB budget as every other transaction; since Android 7 an oversize Bundle crashes the app with TransactionTooLargeException. The practical guideline is a few tens of kilobytes: ids, scroll positions, entered text, which tab. Never a list of items and never a bitmap.
  4. How it comes back. After a kill, the user's return launches the top Activity of the task with the saved Bundle in onCreate(savedInstanceState) and again in onRestoreInstanceState after onStart. Activities lower in the back stack are not recreated yet; each one is instantiated with its own Bundle only when the user backs into it.
flowchart TD
  A1["Activity, pid 4120<br/>onSaveInstanceState"] -->|"sent with onStop"| B["ActivityRecord in system_server<br/>the Intent, plus the saved Bundle<br/>a few KB, held in RAM"]
  B -->|"onCreate(savedInstanceState), getIntent()"| A2["Activity, pid 9877<br/>a new process, same screen"]
Figure 6. The Bundle's journey. It leaves the process with the stop transaction and returns in the launch transaction of a different process. The Intent that started the Activity is kept too, which is why deep-link arguments survive.

Modern code rarely calls onSaveInstanceState directly. The AndroidX saved state registry routes the same Bundle to three places: SavedStateHandle for anything a ViewModel wants to keep, rememberSaveable for Compose state, and Navigation for the back stack. All three end up as keys in the Activity's Bundle and follow its rules and its size limit.

// A ViewModel that survives death: what the user typed lives in the handle, not in a field
@HiltViewModel
class CheckoutViewModel @Inject constructor(
    private val handle: SavedStateHandle,
    private val carts: CartRepository,
) : ViewModel() {
    // written into the Activity's Bundle on every change, restored on recreation
    val promoCode: StateFlow<String> = handle.getStateFlow("promo", "")
    fun onPromoChanged(code: String) { handle["promo"] = code }

    // the cart itself is data, so it is reloaded from disk, never saved into the Bundle
    val cart: StateFlow<Cart?> = carts.observe(handle.get<String>("cartId")!!)
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
}

// Compose: the same idea one level down
@Composable
fun StepIndicator() {
    var step by rememberSaveable { mutableIntStateOf(1) }   // survives rotation and death
    var pulse by remember { mutableStateOf(false) }         // transient, survives neither
    // ...
}
Watch out

rememberSaveable and SavedStateHandle accept anything a Bundle accepts, which includes a whole Parcelable list. That the API allows it does not mean the Binder budget does. Save the id and reload the list.

In the wild
  • Navigation Compose writes the back stack and every destination's arguments through the registry, so a deep screen after a kill comes back with the right route and ids.
  • The 50 KB guideline in the platform documentation is not a hard limit; it is the size below which the Bundle stays cheap to parcel on every stop and safe under the shared Binder budget.
  • Chrome ignores the Bundle almost entirely and restores from its own on-disk tab state, the right choice when the state is far bigger than a Bundle should be.
ActivityRecord
system_server's object for one Activity in a task: its intent, its state, and the saved Bundle.
persistableMode
A manifest attribute that lets an Activity's task, and optionally a PersistableBundle of state, survive a reboot.

The evidence: reading the exit records

Since Android 11 the system remembers why each recent process of an app ended, and an app can read that history at its next start, so the question "who killed my process" has an exact answer instead of a guess.

getHistoricalProcessExitReasons on ActivityManager, given a package, a pid filter and a count, returns a list of ApplicationExitInfo, newest first. Each record carries the reason code from the suspects table, a free-text description written by whoever did the killing, the process's importance at the time (foreground, visible, service, cached), its PSS and RSS in bytes, the timestamp, and for an ANR a stream with the stack traces the system collected. It survives reboots. Reading it on startup and sending it to analytics turns "some users report lost state" into "0.8 percent of sessions on 4 GB devices end with REASON_LOW_MEMORY from the cached state".

// On the next launch: what happened to the last process, and what was it doing
class ExitReporter(private val context: Context) {
    fun report() {
        if (Build.VERSION.SDK_INT < 30) return
        val am = context.getSystemService(ActivityManager::class.java)
        val last = am.getHistoricalProcessExitReasons(null, 0, 1).firstOrNull() ?: return
        val note = last.processStateSummary?.toString(Charsets.UTF_8)   // our own last words, see below
        analytics.log("process_exit",
            "reason" to last.reason,               // REASON_LOW_MEMORY, REASON_USER_REQUESTED, ...
            "description" to last.description,     // "remove task", "stop com.example", ...
            "importance" to last.importance,        // IMPORTANCE_CACHED and friends
            "rssMb" to last.rss / 1_048_576,
            "screen" to note)
        if (last.reason == ApplicationExitInfo.REASON_ANR)
            last.traceInputStream?.use { uploadAnr(it) }
    }
}

// Anywhere: leave up to 128 bytes that will be attached to our death record
fun onScreenShown(route: String) {
    if (Build.VERSION.SDK_INT >= 30)
        context.getSystemService(ActivityManager::class.java)
            .setProcessStateSummary(route.toByteArray())
}

setProcessStateSummary is the underused half of the API. The app stores a short note, up to 128 bytes, and whatever kills the process, that note is attached to the record. Writing the current screen name into it on every navigation means the exit history says not just why the process died but where the user was, which is exactly the "steps to reproduce" a memory kill never comes with.

From the shell

# The same records, without writing code
adb shell dumpsys activity exit-info com.example.app
#   ApplicationExitInfo #0:
#     timestamp=2026-09-10 09:41:12.318 pid=4120 realUid=10231 ...
#     importance=400 reason=3 (LOW_MEMORY) subreason=0 (UNKNOWN) status=0
#     pss=71MB rss=142MB description=null state=checkout/3

# Every kill ActivityManager ordered, with the adj and the reason
adb logcat -b all -s ActivityManager:I | grep -E "Killing|Force stopping|has died"

# lmkd's own account, with the score and how much it freed
adb logcat -s lowmemorykiller

# Reproduce a memory kill on demand: background the app first, then
adb shell am kill com.example.app           # only kills processes that are safe to kill
adb shell am force-stop com.example.app     # the Force stop button, task and all
In the wild
  • Crashlytics and Sentry both read ApplicationExitInfo to report ANRs on Android 11 and later, because the trace stream in the record is the only reliable ANR evidence an app can get without root.
  • Perfetto traces show the same story from above: the lmkd track marks each kill, and the process's own track simply ends.
  • Play Console vitals counts crashes and ANRs but not memory kills; if your metric is "sessions lost", the exit history is the only source.
Tip

am kill refuses to kill a process in the foreground, and Android Studio's stop button removes the task. The recipe that reproduces a real memory kill is: open the screen, press Home, run adb shell am kill, then return through Recents. If the screen comes back correct with the user's input intact, the app passes.

ApplicationExitInfo
A record of one process ending: reason, description, importance, memory, timestamp, optional ANR trace, and the app's own 128-byte summary.
Process state summary
A short byte array the app stores with setProcessStateSummary, attached to its next exit record.

Staying alive, honestly: the ranks you may hold

There is no way to opt out of being killed, but there are components the system ranks highly because they represent something the user can see or hear, and holding one while it is true is how an app legitimately stays up.

You need to...UseRank while it runsRules
Play audio, navigate, track a workout, run a callForeground service with a matching foregroundServiceType and a notificationAbout 200, perceptible, or 0 while the app is in the foregroundSince Android 12 it cannot be started from the background except in listed cases; since Android 14 the type is mandatory and each type has its own permission; Android 15 limits dataSync and mediaProcessing to 6 hours a day. startForeground must follow startForegroundService within a few seconds or the app ANRs.
Finish a short task the user started, such as a message sendExpedited WorkManager jobRuns promptly, process treated as important while runningQuota-limited; falls back to a normal job when the quota is spent.
Serve another app that is in the foregroundBound serviceBorrowed from the client, close to 0Ends when the client unbinds or leaves the screen.
Do deferrable work with constraintsWorkManager, JobSchedulerCached until the job runsTiming follows the app's standby bucket: active, working set, frequent, rare, restricted.
Wake at an exact timeExact alarmCached until it firesNeeds SCHEDULE_EXACT_ALARM (Android 12) or USE_EXACT_ALARM for alarm clocks and calendars; denied by default for most apps since Android 14.
React to a pushFCM high-priority messageA short window of foreground-like importanceQuota-limited; a normal-priority message waits for the next maintenance window in Doze.

Everything else is folklore that stops working release by release: START_STICKY restart loops, a one-pixel "keep alive" Activity, playing silent audio, asking every user to disable battery optimisation. Each one either no longer works, gets the app flagged by the platform's resource police, or trains users to uninstall.

In the wild
  • Spotify and YouTube Music run a mediaPlayback foreground service; the notification with the play controls is the visible promise that earns the rank.
  • Google Maps in turn-by-turn mode and Strava recording a ride hold a location foreground service, and stop it the moment the trip ends.
  • WhatsApp during a call holds phoneCall and microphone types; between calls it holds nothing and relies on high-priority push to wake up.
Watch out

A foreground service is a rank, not immortality. It sits around 200, above every cached process, but under real pressure on a small device even perceptible processes are killed; the service comes back only because the system restarts sticky services, and it comes back with nothing in memory. The next section applies to services as much as to screens.

Foreground service type
A manifest declaration such as mediaPlayback, location or dataSync that says what a foreground service does; mandatory since Android 14 and tied to specific permissions.
Standby bucket
One of five bands, from active to restricted, that the system assigns each app from usage and that governs how often its jobs and alarms run.
Expedited work
WorkManager work that runs as soon as possible with foreground-like importance, subject to a quota.

Writing an app that shrugs it off

An app survives process death when every piece of state has a home outside the process or a way to be rebuilt, so that a fresh process starting on any screen can get back to a correct one without help.

Three tiers of state

  1. UI state goes into saved state. What the user typed, which tab, which item is expanded, the scroll position, the ids of what is on screen. Small, per screen, via rememberSaveable, SavedStateHandle or Navigation arguments.
  2. Data goes to disk. The cart, the draft, the session token, the last sync result. Room, DataStore or files, read through a repository that knows how to load on demand. Memory caches in front of it are fine as long as they are caches, empty on start and filled by a read.
  3. Everything else is recomputed. Derived values, animation state, the connection to a service, an in-flight request. It is lost, and the screen must be able to say "loading" and fetch again.

Habits that follow

Testing it

Rotation tests the left branch of Figure 5 and proves nothing about the right one. "Don't keep activities" in developer options destroys the Activity on every leave but keeps the process, so ViewModels survive and the test is still the wrong one. Two things test the real path:

@Test
fun promoCodeSurvivesRestoration() {
    val tester = StateRestorationTester(composeRule)
    tester.setContent { CheckoutScreen() }
    composeRule.onNodeWithTag("promo").performTextInput("SEPTEMBER")
    tester.emulateSavedInstanceStateRestore()               // save, tear down, restore from the Bundle
    composeRule.onNodeWithTag("promo").assertTextEquals("SEPTEMBER")
}
In the wild
  • Hilt injects SavedStateHandle into any @HiltViewModel for free, which removes the last excuse for keeping form state in fields.
  • Jetpack Navigation with typed routes makes "ids, not objects" the path of least resistance: the route is a string and the arguments are serialisable.
  • WorkManager is the model citizen: it assumes the process that enqueued the work is gone by the time the work runs, and everything about its design follows from that.

Mistakes that keep coming back

Recap

  • Process death is the system ending a background process and later recreating the top Activity from saved state; the screen looks resumed, the heap is gone, and no crash reporter sees it.
  • Every kill has a reason code in ApplicationExitInfo: low memory, user swipe, force stop, permission change, package update, ANR, crash, dependency died, freezer, excessive resource usage.
  • lmkd kills on memory pressure by oom_score_adj: 0 foreground, 100 visible, 200 perceptible, 500 service, 600 home, 700 previous, 900 to 999 cached. A process ranks by its best component; cached processes go first, largest first.
  • A swipe removes the task but keeps alarms, jobs and receivers; a force stop puts the app in the stopped state where nothing runs until the user opens it. Many vendor battery managers force stop.
  • Revoking a permission, updating the package, a provider you hold a cursor on dying, and a synchronous Binder call into a frozen process all end the process too.
  • Survives: the saved Bundle, the task and back stack, disk, alarms and jobs, WorkManager, posted notifications. Gone: Activity, ViewModel, remember, singletons, Application fields, in-memory registrations.
  • ViewModel survives rotation only; rememberSaveable and SavedStateHandle survive both; remember survives neither.
  • Saved state is written with onStop, kept in system_server's ActivityRecord, travels over Binder under the shared 1 MB budget, and comes back in onCreate; lower activities are recreated lazily.
  • getHistoricalProcessExitReasons gives reason, description, importance, memory and ANR traces; setProcessStateSummary attaches your own 128-byte note to the record.
  • The honest ranks are a typed foreground service, expedited work, a binding from a foreground client, and scheduled jobs and alarms within their rules; keep-alive tricks no longer work.
  • UI state to saved state, data to disk, everything else recomputed; ids not objects; every screen able to be the first; Application.onCreate idempotent.
  • Test with Home, am kill, Recents, and with StateRestorationTester; rotation and "Don't keep activities" test the wrong path.

Questions

Try answering each one out loud before opening it. Lead with the one-line answer, then the reasoning, then one detail that shows you have used it.

What is process death, and how does it differ from a crash and from a configuration change?

Process death is the system killing an app's process while it is in the background and later recreating its top Activity from saved state; a crash ends the process from inside with an exception, and a configuration change recreates the Activity inside the same living process.

  • After a configuration change, ViewModels, singletons and the Application survive; after process death only saved state and disk do.
  • A crash is visible to the user and to crash reporters; a kill is neither.
  • The recreated process has a new pid and runs Application.onCreate again.

The user's description of process death is always "I came back and it had forgotten", because the screen was restored and the data was not.

How does the low memory killer choose which process to kill?

lmkd watches memory pressure and kills the process with the highest oom_score_adj, a score ActivityManager assigns from the most important component the process holds, taking the largest process among equals.

  • Roughly 0 for the foreground app, 100 visible, 200 perceptible, 500 service, 600 home, 700 previous app, 900 to 999 cached.
  • It is a user-space daemon since Android 9, triggered by PSI on Android 10 and later; the kernel OOM killer is only the last resort.
  • Thresholds are per device, so small-RAM phones kill far earlier.

A bound service borrows its client's rank, which is why an SDK service is rarely killed while its host app is on screen.

What is oom_score_adj, who sets it, and what does a "cached" process mean?

It is a per-process integer from -1000 to 999 in /proc that both the kernel OOM killer and lmkd use to pick victims; ActivityManager computes it from the process's components and lmkd writes it; a cached process has no active component and sits at 900 or above.

  • The score is recomputed whenever a component's state changes.
  • Cached processes are kept in an LRU list whose scores rise with age.
  • Being cached is the normal state of every app the user just left.

dumpsys activity processes shows the current value and the state abbreviation, such as CAC or TOP, for every process.

What is the difference between a user swiping the app from Recents and pressing Force stop?

A swipe removes the task and kills the process but leaves alarms, jobs, manifest receivers and push working; a force stop also puts the app in the stopped state, where nothing runs until the user opens it again by hand.

  • After a swipe the next launch is cold at the launcher activity because the task is gone.
  • After a force stop, BOOT_COMPLETED, alarms and FCM are all withheld.
  • Many vendor battery managers implement "sleeping apps" as a force stop.

The exit records distinguish them as REASON_USER_REQUESTED with description "remove task" versus REASON_USER_STOPPED.

Name three system reasons, other than memory, that end a process, and what each implies.

A runtime permission change kills the process so it cannot run with stale permission state; a package update kills it because the code changed and then sends MY_PACKAGE_REPLACED; a dependency dying kills it because it held a stable reference to a content provider whose process died.

  • Also: a synchronous Binder call into a frozen cached process, and excessive background CPU or too many phantom child processes since Android 12.
  • An ANR in the background is a silent kill.
  • The task and saved state survive all of these except the update, which removes the task.

Holding a Cursor open is enough to make a provider crash yours, which is why cursors should be read and closed.

Which state survives process death, and which does not?

The saved instance Bundle, the task and back stack with each Activity's Intent, disk, scheduled alarms and jobs, WorkManager work and posted notifications survive; every object in the heap does not, including ViewModels, remember values, singletons, Application fields and context-registered receivers.

  • SavedStateHandle and rememberSaveable survive because they write into the Bundle.
  • Bound service connections are dropped and re-established by the system for auto-create clients.
  • A force stop additionally cancels alarms and jobs.

The dangerous rows are the ones that survive rotation but not death, because rotation is what everyone tests.

Why does a ViewModel survive rotation but not process death?

Because across a configuration change the old Activity hands its ViewModelStore to the new one through NonConfigurationInstances, an in-process object, and process death destroys the process that object lives in.

  • SavedStateHandle exists to carry the small part of a ViewModel's state that must outlive the process.
  • Data the ViewModel holds should be reloadable from a repository.
  • The same logic applies to Compose's remember versus rememberSaveable.

Hilt injects SavedStateHandle into any @HiltViewModel, so there is no extra plumbing to use it.

When is onSaveInstanceState called, where does the Bundle live, and how large may it be?

It is called with onStop (after it since API 28) whenever the Activity may be destroyed and later restored, the Bundle is kept in system_server's ActivityRecord in memory, and it must stay small because it crosses Binder under the shared 1 MB budget.

  • It is not called when the user finishes the Activity with Back.
  • Since Android 7 an oversize Bundle crashes the app with TransactionTooLargeException; the guideline is tens of kilobytes.
  • It is lost on reboot unless the Activity uses persistableMode with a PersistableBundle.

Activities below the top of the task are not recreated on return; each is instantiated with its own Bundle only when the user backs into it.

How would you find out how often, and why, real users' processes are being killed?

Read ActivityManager.getHistoricalProcessExitReasons on startup on Android 11 and later and log each record's reason, description, importance and memory to analytics, with setProcessStateSummary used during the session to tag the current screen.

  • Reasons separate memory kills from user swipes, force stops, ANRs and crashes.
  • Importance says whether the process was cached, a service or visible when it died.
  • The ANR records include a trace stream.

Crash reporters do not count memory kills, so this is the only source for a "sessions lost to process death" metric.

What is setProcessStateSummary for?

It stores up to 128 bytes of the app's own choosing that the system attaches to the app's next exit record, so the app can leave a note such as the current screen and read it back after the kill.

  • Update it on every navigation and the exit history says where users were when they were killed.
  • It survives every kind of kill because system_server holds it.
  • It is readable through dumpsys activity exit-info as well.

Combined with the reason code, it is the closest thing a memory kill has to a stack trace.

How do you reproduce process death on a device, and why are rotation and "Don't keep activities" not enough?

Open the screen, press Home, run adb shell am kill <package>, and return through Recents; rotation and "Don't keep activities" destroy the Activity but keep the process, so ViewModels and singletons survive and the test never exercises the real path.

  • am kill only kills background processes, so the app must be backgrounded first.
  • Android Studio's stop button force-stops and removes the task, which tests a cold start instead.
  • The "Background process limit" developer option turns every Home press into a kill.

In tests, Compose's StateRestorationTester exercises the saved state path, and ActivityScenario.recreate() covers the Bundle but not the singleton problem.

What legitimately raises a process's rank, and what are the rules around foreground services now?

A foreground service with a declared type and notification, a binding from a foreground client, and expedited work all raise the rank; since Android 12 a foreground service cannot be started from the background except in listed cases, since Android 14 its type is mandatory with per-type permissions, and Android 15 limits dataSync and mediaProcessing to six hours a day.

  • startForeground must follow startForegroundService promptly or the app ANRs.
  • The rank is about 200, above cached but still killable under real pressure.
  • Deferrable work belongs in WorkManager, timed work in alarms with the exact-alarm permission where needed.

Spotify's playback notification is the visible promise that earns its mediaPlayback rank; a service with nothing to show is a keep-alive and fails review.

What is the cached app freezer, and how can it cause a kill?

Since Android 12 cached processes are placed in a cgroup freezer so they consume no CPU; the system thaws a process before delivering work, but a synchronous Binder call made directly into a frozen process cannot complete, and the system may kill the process with REASON_FREEZER.

  • Freezing is invisible to the app: its memory stays, its threads simply do not run.
  • Oneway calls are queued and delivered after thawing.
  • It is why a leaked timer in a background app no longer drains the battery.

A bound service that a foreground client keeps calling is never frozen, because the binding keeps it out of the cached band.

Design the state handling for a multi-step checkout so that it survives process death.

Keep the cart and the order draft in a repository backed by Room or DataStore, keyed by an id; put the current step and the user's in-progress input in SavedStateHandle or rememberSaveable; pass the cart id, not the cart, as the navigation argument; and make each step's screen able to load everything it needs from the id alone.

  • Never hold the cart only in a shared ViewModel or a singleton.
  • Keep saved state to ids and small fields; reload lists.
  • Show a loading state while the repository reads, because after a kill it will.

Test it by backgrounding on step three with a promo code typed, am kill, and returning: the code, the step and the cart must all be there.

Why does the same app lose state constantly on one tester's phone and never on yours?

Because lmkd thresholds and the cached process cap are tuned per device, so a phone with 4 GB and an aggressive vendor battery manager kills cached apps within minutes while a 12 GB developer phone keeps dozens alive.

  • Android Go and low-RAM devices set the most aggressive thresholds.
  • Vendor sleepers add force stops on top of memory kills.
  • Heavy use of the camera or a game between sessions raises pressure sharply.

Reading the exit records from that tester's device shows the reason and the memory at the time, which settles the argument in one line.

Why is a static "holder" for passing large objects between screens a bug, and what replaces it?

Because the holder lives in the heap and is empty after process death, so the destination screen, recreated first, reads null; the replacement is passing an id and loading the object from a repository, with anything unsaved written to disk first.

  • Putting the object in the Bundle instead trades a null for a TransactionTooLargeException.
  • Navigation arguments should be small and serialisable.
  • A destination that can be the first screen after a restore is the design goal.

This is the same discipline that makes deep links work, so an app that handles deep links well usually handles process death well.