AI-powered crash analysis is now available on all plans — including Free.Read the crash analysis guide

Mobile App Process Death & OS Kill Detection

NFNourin Mahfuj Finick··10 min read

Mobile app crashes usually mean bugs in your code — null pointer dereferences, out-of-bounds array access, or unhandled exceptions that produce a recognizable stack trace. But a significant portion of "crashes" reported by users are not crashes at all. They are OS-initiated process deaths: the Android Low Memory Killer (LMK) or iOS Jetsam terminating your app to reclaim memory for foreground processes. Understanding the distinction between a genuine code crash and an OS kill is essential for accurate mobile app process death analytics, yet most crash reporting tools struggle to tell them apart. In this guide, we cover how Android LMK and iOS Jetsam work, how to detect process death events programmatically, and how to instrument your app so your crash reporting tool knows the difference between "the app crashed" and "the OS killed it."

How Android Low Memory Killer Works

The Android Low Memory Killer (LMK) is a kernel-level mechanism that terminates processes when the system runs low on free memory. Unlike a typical crash that throws an exception, LMK sends SIGKILL (signal 9) directly to your app process — no handler fires, no stack trace, no last words. The kernel maintains an oom_score_adj value for every process, and when memory pressure crosses a configured threshold, it kills the highest-scored eligible process first.

Android's LMK uses a multi-tier classification system. Foreground apps have the lowest oom_score_adj value (typically 0), making them last to be killed. Visible but not focused processes — such as an app behind a translucent activity — get slightly higher scores. Background services, cached processes, and empty processes are progressively more likely targets. If your app caches large images in the background or retains big bitmaps in a long-running service, it becomes a prime candidate for LMK termination.

Android exposes memory pressure information through the ComponentCallbacks2 interface and ActivityManager. The onTrimMemory(int level) callback fires at different severity levels, from TRIM_MEMORY_UI_HIDDEN (mild) to TRIM_MEMORY_RUNNING_CRITICAL, which signals imminent process termination. Responding aggressively — clearing caches, releasing bitmaps, flushing non-essential state — can keep your app alive under memory pressure.

@Override
public void onTrimMemory(int level) {
    super.onTrimMemory(level);
    if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW) {
        // Moderate memory pressure — release caches
        clearImageCache();
        releaseUnusedBitmaps();
    }
    if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) {
        // Severe memory pressure — process may be killed next
        persistCriticalState();
        clearAllCaches();
        logMemoryPressureEvent(level);
    }
}

Detecting Android LMK Kills on Next Launch

Since LMK sends SIGKILL, you cannot catch or handle it. But you can detect that the previous session was killed by checking persistent state on the next cold start. One common technique — known as the "deadman switch" pattern — involves writing a sentinel flag to persistent storage at app startup and clearing it during normal shutdown. If the flag is still present on the next launch, the previous session did not shut down gracefully. When combined with other signals — such as the absence of a crash report in your crash reporting tool and recorded memory pressure events — you can infer an OS kill with reasonable confidence.

class ProcessDeathDetector(private val prefs: SharedPreferences) {
    companion object {
        private const val KEY_APP_ALIVE = "app_alive_flag"
        private const val KEY_LAST_DEATH_TYPE = "last_death_type"
        private const val KEY_LAST_MEMORY_PRESSURE = "last_memory_pressure_level"
    }
 
    fun markAppAlive() {
        prefs.edit().putBoolean(KEY_APP_ALIVE, true).apply()
    }
 
    fun markCleanShutdown() {
        prefs.edit().putBoolean(KEY_APP_ALIVE, false)
            .putString(KEY_LAST_DEATH_TYPE, "clean").apply()
    }
 
    fun checkPreviousSessionDeath(): String? {
        val wasAlive = prefs.getBoolean(KEY_APP_ALIVE, false)
        if (!wasAlive) return null
 
        // Previous session was alive and never shut down cleanly
        val pressureLevel = prefs.getInt(KEY_LAST_MEMORY_PRESSURE, -1)
        return when {
            pressureLevel >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL ->
                "os_kill_critical_memory"
            pressureLevel >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW ->
                "os_kill_memory_pressure"
            else -> "os_kill_unknown"
        }
    }
}

iOS Jetsam: How Apple Handles Memory Pressure

On iOS, the equivalent mechanism is called Jetsam. When the system detects critically low memory, it sends didReceiveMemoryWarning to running applications. If apps don't free enough memory — or if pressure is severe, as when Camera or a large game runs in the foreground — Jetsam may terminate background and suspended apps directly with SIGKILL. iOS 15+ introduced MetricKit, which delivers structured diagnostic payloads including Jetsam events and termination reason codes.

Unlike code crashes that generate crash reports in Xcode Organizer, Jetsam terminations produce system diagnostic logs that are harder to access. On a development device, you can retrieve these through Xcode's Devices window. In the field without MetricKit, Jetsam terminations are invisible to most crash reporting SDKs — a blind spot in your app's stability data.

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
 
    let memoryFootprint = memoryFootprintInMB()
    Logger.processDeath.warning(
        "Memory warning received. Current footprint: \(memoryFootprint) MB"
    )
 
    // Aggressively release caches
    imageCache.removeAllObjects()
    dataCache.removeAllObjects()
 
    // Flush non-critical state
    UserDefaults.standard.synchronize()
}
 
private func memoryFootprintInMB() -> Double {
    var info = mach_task_basic_info()
    var count = mach_msg_type_number_t(
        MemoryLayout<mach_task_basic_info>.size / 4
    )
    let result = withUnsafeMutablePointer(to: &info) {
        $0.withMemoryRebound(to: integer_t.self, capacity: 1) {
            task_info(
                mach_task_self_,
                task_flavor_t(MACH_TASK_BASIC_INFO),
                $0, &count
            )
        }
    }
    return result == KERN_SUCCESS
        ? Double(info.resident_size) / (1024.0 * 1024.0)
        : 0
}

Detecting iOS Jetsam Events in Production

Apple provides two primary mechanisms for detecting OS kill events in production. The first is MetricKit's MXCrashDiagnostic API (iOS 14+), which delivers diagnostic payloads including termination reason codes. When Jetsam kills your app, the termination reason appears as a string like "JETSAM" or a memory-related exit code rather than an exception type. The second approach mirrors the Android deadman switch: write a flag to UserDefaults at launch, clear it during applicationWillTerminate(_:), and check its presence on the next cold start. If the flag remains set and no crash diagnostic was delivered, you have strong evidence for a system-initiated kill.

import MetricKit
 
class ProcessDeathMonitor: NSObject, MXMetricManagerSubscriber {
    override init() {
        super.init()
        MXMetricManager.shared.add(self)
    }
 
    func didReceive(_ payloads: [MXDiagnosticPayload]) {
        for payload in payloads {
            if let crashDiagnostics = payload.crashDiagnostics {
                for crash in crashDiagnostics {
                    if let reason = crash.terminationReason {
                        Logger.processDeath.info(
                            "Termination reason: \(reason)"
                        )
                        // Jetsam kills show memory-related termination reasons
                        // rather than exception type codes
                    }
                }
            }
        }
    }
 
    func detectPreviousSessionDeath() -> Bool {
        guard let wasAlive = UserDefaults.standard
            .object(forKey: "lastSessionAlive") as? Bool,
              wasAlive == true else {
            return false
        }
        // Previous session was alive and did not shut down cleanly
        // If no crash diagnostic was received, this is likely Jetsam
        return true
    }
}

System Kill vs. Code Crash: Telling Them Apart

Distinguishing between OS-initiated process death and code-level crashes is critical for accurate crash rate calculation. If your crash reporting tool lumps LMK kills and Jetsam events with NullPointerExceptions and SIGABRTs, your crash rate will be inflated — and your team will waste time investigating "crashes" not fixable in application code. Here is a practical framework for differentiation.

Code crash indicators: A stack trace is generated and captured, the crash reporter SDK uploads the report, the signal is typically SIGSEGV, SIGABRT, or SIGBUS, and an uncaught exception handler fires — UncaughtExceptionHandler on Android or NSSetUncaughtExceptionHandler on iOS.

OS kill indicators: No stack trace or crash report exists, the process receives SIGKILL (which cannot be intercepted), termination typically occurs in the background or while suspended, and memory-related system events precede the termination. On Android, you see "Process X died" in logcat without a crash tombstone. On iOS, systemmemoryreset events appear in diagnostic logs.

For crash reporting platforms like Bugspulse, implementing this distinction means correlating lifecycle events — start, foreground, background, terminate — with crash submissions. A process death event with no crash report, occurring while suspended or in the background, is almost certainly an OS kill. Bugspulse records these as separate "process death events," giving teams accurate crash rates alongside process health metrics.

Instrumenting Your Crash Reporting Tool

Most mobile crash reporting tools — including Firebase Crashlytics and Sentry — capture uncaught exceptions via signal handlers. They can't distinguish SIGKILL (system kill) from SIGSEGV (code crash) because SIGKILL cannot be handled by design. To add process death detection, you need out-of-band mechanisms.

First, implement a launch-time state check. On every cold start, inspect whether the previous session ended cleanly. If the deadman flag is still set, flag it as a potential OS kill and submit it to your crash reporting tool as a custom event or breadcrumb.

Second, log memory pressure events. Record every onTrimMemory call on Android and every didReceiveMemoryWarning call on iOS, each with a timestamp. If a process death follows a severe memory warning, you have strong evidence for an OS kill. See our guide on mobile app OOM crash debugging for a deeper look at memory-related diagnostics.

Third, correlate with crash reports. If your crash reporting SDK uploaded a crash report for the previous session, it was a code crash. No report plus a deadman flag equals an OS kill. This correlation should run at the start of each new session.

Finally, use system frameworks directly. MetricKit on iOS provides authoritative termination reasons in MXCrashDiagnostic. On Android, ActivityManager.getHistoricalProcessExitReasons() (API 30+) returns structured exit reason data including REASON_LOW_MEMORY, making the distinction unambiguous on newer devices.

// Android: Exit reason API for definitive OS kill detection (API 30+)
import android.app.ActivityManager;
import java.util.List;
 
public class ExitReasonDetector {
    public static boolean wasPreviousSessionOomKilled(ActivityManager am) {
        List<ActivityManager.ApplicationExitInfo> exitReasons =
            am.getHistoricalProcessExitReasons(null, 0, 1);
        if (exitReasons.isEmpty()) return false;
 
        ActivityManager.ApplicationExitInfo lastExit = exitReasons.get(0);
        int reason = lastExit.getReason();
 
        return reason == ActivityManager.REASON_LOW_MEMORY
            || reason == ActivityManager.REASON_CRASH_NATIVE
            && lastExit.getStatus() == 9; // SIGKILL
    }
}

Best Practices for Reducing OS Kill Risk

While you can't prevent the OS from killing processes under extreme memory pressure, you can significantly reduce the likelihood and improve the user experience when it happens.

Minimize memory footprint. Use memory profiling tools — Android Studio's Memory Profiler and Xcode's Memory Graph Debugger — to identify leaks and excessive allocations. Large bitmap caches, WebView instances, and media decoders are common culprits. Release them aggressively in onTrimMemory and didReceiveMemoryWarning.

Persist state early and often. If your app gets killed, users should lose as little work as possible. Save draft state in onStop (Android) or applicationDidEnterBackground(_:) (iOS), not in onDestroy or applicationWillTerminate(_:) — those methods may never be called during an OS kill.

Reduce background memory usage. Background services with large memory footprints are the top target for LMK and Jetsam. Offload heavy work to WorkManager on Android or BGTaskScheduler on iOS, which let the system schedule work opportunistically when resources are available.

Monitor and alert. Once you have instrumented process death detection, set up alerts when your OS kill rate spikes. A sudden increase in LMK or Jetsam events without a corresponding code change often signals a server-side regression — such as loading larger images or processing larger API payloads — that increases your app's memory pressure.

Test under memory pressure. Use adb shell am kill on Android or Xcode's memory pressure simulation to verify that your state persistence and detection mechanisms work correctly in a controlled environment before deploying to production.

Wrapping Up

OS-initiated process death — whether from Android's Low Memory Killer or iOS Jetsam — is not a bug you can fix with a code patch. It is a system behavior you must detect, measure, and design around. By implementing deadman switch detection, memory pressure logging, and MetricKit or ActivityManager correlation, you can separate true crashes from system kills and get an accurate picture of your app's stability.

For teams looking to track process death events alongside traditional crash reporting, our mobile app background crash detection guide covers related patterns for background termination scenarios. Understanding when the OS killed your app — versus when your code genuinely crashed — is the difference between chasing phantom bugs and making informed engineering decisions.

Start tracking process death events today on Bugspulse — the privacy-first crash reporting platform that distinguishes system kills from code crashes.