How iOS decides which app to kill when memory is low
How Apple's Jetsam memory pressure daemon ranks suspended apps by priority, RSS size, and foreground recency to reclaim memory without user-visible crashes.
The scenario
An iPhone is under memory pressure while one app is active and many others are suspended. The system must reclaim memory without making the phone unresponsive, but it cannot rely on desktop-style swap to keep every process resident.
iOSβs Jetsam/memorystatus machinery combines process importance with physical memory footprint. The exact bands and limits vary by device and OS release, so the durable lesson is the policy shape, not a memorized number.
30-second mental model
Apps move through lifecycle states such as active, background, and suspended. When pressure becomes severe, the system prefers less important processes and uses their physical footprint to choose efficient victims; an active app is protected as far as the system can manage. Memory warnings are an opportunity to release caches, not a guaranteed last callback before termination.
5-minute end-to-end flow
- The app enters the background, saves durable state, releases recreatable caches, and may be suspended.
- The VM subsystem tracks physical footprint and system pressure across processes.
- As pressure rises, iOS may issue memory warnings and reclaim clean file-backed pages.
- If pressure remains critical, Jetsam selects a lower-priority process, typically preferring a large reclaimable footprint within that priority class, and terminates it with SIGKILL.
- On the next launch, the app restores from durable state rather than assuming its old process survived.
- Engineers validate the behavior on representative devices with Instruments, MetricKit, and device JetsamEvent diagnostics.
The Architecture
Here is the full picture of how memory management works on iOS, from the user tapping an app to Jetsam making a kill decision:
Let me walk through this step by step.
When the user opens an app, it becomes the foreground process with the highest priority band (around 800 in Jetsam's scheme). The moment the user swipes home or switches to another app, the original app transitions to "foreground suspended," a grace period where it is still in memory but frozen. After roughly 10 seconds of no activity, it drops to the fully "suspended" state with a much lower priority band.
The Jetsam daemon continuously monitors the system's memory pressure through the VM subsystem. Every process has a memory ledger that tracks its physical footprint (RSS, or Resident Set Size). When total memory usage crosses a threshold, Jetsam starts killing processes, starting from the lowest priority band.
For your interview: the key phrase is "priority bands, not recency." iOS does not simply kill the least recently used app. It kills the lowest-priority, highest-memory app first.
A common misconception: developers assume iOS uses an LRU (Least Recently Used) policy because that is how most caches work. Jetsam is not a cache eviction system. It is a process termination system. The goal is not "remove stale entries" but "free the maximum amount of memory with minimum user impact." Priority bands achieve this better than recency because they encode user-facing importance, not just time.
Here is how to think about the flow between layers. The hardware layer is simple: physical RAM is a finite, shared resource. The kernel's VM subsystem tracks how much of that resource each process is using. The Jetsam daemon sits between the kernel and the app layer, acting as a resource arbiter. It reads pressure signals from below and sends warnings (or kills) upward.
The critical thing to notice: there is no feedback loop from apps back to Jetsam. When an app releases memory in response to a warning, Jetsam does not receive a "I freed some memory" message. It simply re-evaluates the system state on its next scan cycle. If the pressure has dropped below the threshold, it stops killing. If not, it continues. This is a pull model, not push.
The Jetsam Priority Band System
The priority band system is the core of Jetsam's decision making. Every process on the system is assigned a priority band, and within each band, processes are ranked by memory footprint. This two-dimensional ranking (priority band first, memory size second) is what makes the system precise rather than guessing.
Here is what each band contains:
Band 0-50 (Idle daemons): System daemons that are not doing anything. These are killed first because they can be relaunched on demand by launchd. The user never notices.
Band 50-150 (Suspended apps): Apps the user backgrounded a while ago. They are frozen (no CPU time) and sitting in RAM. Within this band, the app with the highest RSS gets killed first. If you opened a game that allocated 1.5GB and then switched away, that game is the first suspended app to die.
Band 150-300 (Background utility): Apps doing background fetch, content sync, or push notification processing. They have active work but are not visible.
Band 300-500 (Background active): Apps playing audio, tracking location, or handling VoIP calls. These apps are providing ongoing value to the user even though they are not visible. Killing a navigation app mid-drive would be a terrible experience.
Band 500-700 (Foreground suspended): The app the user just left. It gets a brief grace period at a high priority in case the user immediately switches back.
Band 700-1000 (Foreground active): The app the user is looking at right now. Jetsam kills the foreground app only as an absolute last resort, and if it does, you see a crash.
The priority bands are not fixed integers you can look up in documentation. Apple keeps the exact values internal and changes them between iOS versions. The ranges I am using here are approximations based on kernel source code analysis and debugging with memory_pressure tools. In an interview, describe the bands conceptually (foreground, background, suspended) rather than citing exact numbers.
Memory Pressure Response Lifecycle
When memory pressure starts building, Jetsam does not immediately start killing. There is a graduated response that gives apps a chance to reduce their footprint voluntarily. Understanding this lifecycle is critical because it explains why some apps survive memory pressure while others get killed.
The lifecycle has three pressure levels:
Normal pressure: Everything is fine. All apps stay in memory. The system has enough free RAM for new allocations.
Warning pressure: Available memory drops below a threshold (roughly 200MB on modern iPhones, though it varies by device). Jetsam sends didReceiveMemoryWarning to the foreground app. This is the app's chance to release cached images, drop reusable views, clear undo history, and generally slim down its footprint. The app is not being killed. It is being asked to cooperate.
Critical pressure: Available memory drops below a critical threshold (roughly 50-80MB). Jetsam starts sending SIGKILL signals. There is no graceful shutdown. The process is terminated immediately. No finalizers run, no state is saved, no callbacks fire. The process is simply gone.
I want to emphasize: SIGKILL means instant death. Apps do not get a chance to save state at this point. This is why smart apps save state proactively during applicationDidEnterBackground, not when they receive a memory warning. By the time you get the warning, it might be too late to do anything meaningful.
Here is the timeline a typical app experiences during memory pressure:
- T+0s: System memory drops below warning threshold
- T+0.1s: Jetsam sends
didReceiveMemoryWarningto the foreground app - T+0.2s-T+2s: Foreground app releases caches (if it implements the handler)
- T+0.5s: Jetsam evaluates all processes and starts killing from the bottom band
- T+0.5s-T+1s: Suspended apps with large footprints die (SIGKILL, instant)
- T+1s: Re-evaluate. If pressure is relieved, stop. If not, continue up the bands.
- T+2s+: If all lower bands are exhausted and pressure is still critical, background apps start dying
- Never (hopefully): Foreground app killed only as nuclear option
The entire process from pressure detection to first kill is typically under 500 milliseconds. Jetsam is designed to be fast because the alternative is the system becoming unresponsive.
Strong candidates mention that iOS uses SIGKILL, not SIGTERM. SIGKILL cannot be caught or ignored. The process has zero opportunity to clean up. This is why apps must save critical state when entering the background, not when receiving a memory warning. The warning is your "heads up," not your "last chance."
How Apps Survive Memory Pressure
This is the most practical deep dive for a mobile engineering interview. The interviewer wants to know: if you were building an iOS app, what would you do to avoid being killed?
The answer comes down to three strategies: reduce your baseline footprint, respond to memory warnings aggressively, and use memory-mapped files instead of heap allocations for large data.
Here is what the proactive approach looks like in practice:
The real lesson here is the distinction between clean and dirty memory. Clean memory (file-backed via mmap) can be silently reclaimed by the kernel. The kernel just drops the pages, and if the app accesses them again, they are faulted back in from disk. Dirty memory (malloc/heap) cannot be reclaimed without killing the process. Smart apps keep their dirty footprint small and push large data into clean, file-backed memory.
How iOS Differs from Android
This is a common follow-up question, and getting it right shows breadth of knowledge.
Android has compressed swap (zRAM). Android uses a portion of RAM as a compressed swap area. When an app's pages are cold, Android compresses them in-place rather than killing the process. This means Android can hold more apps in memory (compressed) before needing to kill any. The trade-off is CPU overhead for compression and decompression.
Android uses oom_adj scores, not priority bands. Every Android process gets an oom_adj score from -1000 (never kill) to 1000 (kill first). The Low Memory Killer Daemon (LMKD) uses these scores with multiple memory pressure thresholds. At each threshold, it kills processes above a certain oom_adj score. This is functionally similar to Jetsam's bands but uses a single linear scale instead of discrete bands.
Android's memory warnings are different. Android sends onTrimMemory() with graduated levels (TRIM_MEMORY_RUNNING_LOW, TRIM_MEMORY_COMPLETE, etc.), giving apps more granular information about the severity of pressure. iOS has only a single didReceiveMemoryWarning with no severity indicator.
Android allows processes to be saved and restored. Android's activity lifecycle is designed around the OS killing and recreating activities. The onSaveInstanceState / onRestoreInstanceState pattern is built into the framework. iOS does not have this at the OS level (though apps can implement state restoration manually).
The bottom line for interviews: iOS is more aggressive because it has no swap. Android is more forgiving because zRAM gives it a buffer. But both systems ultimately kill processes when memory is exhausted. The mechanisms just differ in granularity and aggressiveness.
Here is a side-by-side comparison for quick reference:
| Dimension | iOS (Jetsam) | Android (LMKD) |
|---|---|---|
| Swap | None. Physical RAM only | zRAM compressed swap |
| Kill priority | Discrete priority bands (0-1000) | Linear oom_adj score (-1000 to 1000) |
| Kill signal | SIGKILL always | SIGKILL (plus onTrimMemory before) |
| Memory warning | Single didReceiveMemoryWarning | Graduated onTrimMemory() levels |
| State restoration | Manual (app must implement) | Built into Activity lifecycle |
| Background app limit | Enforced per-process (Jetsam limit) | Enforced per-process + cached process limit |
| GPU memory | Unified memory, counts toward footprint | Separate GPU memory pool on most devices |
| Compression | Limited in-memory compression (iOS 7+) | Full zRAM compressed swap |
| Kill aggressiveness | High (no buffer before killing) | Lower (compression absorbs some pressure) |
| Latency after kill | Fast relaunch from cold | Slower (decompress from zRAM first) |
This table is a strong interview artifact. If the interviewer asks "how does iOS differ from Android in memory management," walking through 3-4 rows of this comparison demonstrates breadth without monologuing.
A strong follow-up in mobile interviews: "If iOS has no swap but Android does, why do iOS apps feel smoother?" The answer is that killing and relaunching is often faster than decompressing cold pages from zRAM. iOS trades memory capacity for latency consistency. Android trades latency for capacity.
Memory Limits by Device
Understanding the per-device Jetsam limits is essential for real-world iOS development and a strong differentiator in interviews. These values are approximate, derived from empirical testing and kernel source analysis, because Apple does not publish them.
| Device | Total RAM | Approx. Foreground Jetsam Limit | Approx. Extension Limit | Notes |
|---|---|---|---|---|
| iPhone SE (2nd gen) | 3 GB | ~1.0-1.2 GB | ~100 MB | Lowest-tier modern device. Memory-intensive apps crash frequently |
| iPhone 12 | 4 GB | ~1.8-2.0 GB | ~120 MB | Baseline for most production apps targeting iOS 16+ |
| iPhone 13 Pro | 6 GB | ~2.8-3.0 GB | ~120 MB | Comfortable headroom for most use cases |
| iPhone 14 Pro | 6 GB | ~2.8-3.0 GB | ~120 MB | Same RAM as 13 Pro, similar limits |
| iPhone 15 Pro Max | 8 GB | ~3.5-4.0 GB | ~120 MB | Highest consumer limit. Games and creative apps benefit |
| iPad Pro M2 | 8 or 16 GB | ~5.0-6.0 GB | ~120 MB | Multitasking with Split View means two foreground apps sharing headroom |
Note the extension limits barely change across devices. Whether you are on a 3GB iPhone SE or an 8GB iPhone 15 Pro, your share extension still gets roughly 100-120MB. This is a deliberate design choice: extensions are meant to be lightweight, and Apple enforces that constraint uniformly.
For interviews, you do not need to memorize exact numbers. The important insight is that the Jetsam limit scales with device RAM but is always well below the total. On a 6GB device, frontend apps get roughly half. The rest is reserved for the kernel, system daemons, the GPU compositor, and background processes.
Bottlenecks, failure modes, and operations
-
Per-process memory limits are not documented. Apple enforces per-process memory limits (the "Jetsam limit"), but these limits vary by device model and iOS version. On an iPhone 15 Pro with 8GB RAM, a foreground app might get up to 3-4GB. On an iPhone SE with 3GB, the limit might be 1.2GB. Third-party developers have to discover these limits empirically, not from documentation.
-
Extensions share your memory budget. If your app uses a share extension, keyboard extension, or widget, those extensions count against separate, much lower memory limits (typically 100-120MB). A common bug is loading a full-resolution image in a share extension, exceeding the limit, and getting killed before the share completes.
-
GPU memory is part of your footprint (mostly). On Apple Silicon, the CPU and GPU share a unified memory architecture. Large textures and render buffers count toward your process's physical footprint. A 3D game might have "only" 200MB of heap allocations but 800MB of GPU textures, putting it well over the Jetsam limit.
-
Background app refresh is a separate budget. Even if your app survives memory pressure, its background refresh opportunities are throttled by a separate system (BackgroundTasks framework). An app that uses too much memory during background refresh gets its background privileges revoked entirely.
-
Jetsam kills are invisible to crash reporters. When Jetsam kills an app, it does not produce a crash report in the traditional sense. It produces a "JetsamEvent" log that most crash reporting tools (Crashlytics, Sentry) do not capture. Many developers think their app "never crashes" when it is actually being killed by Jetsam dozens of times a day.
-
Compressed memory is not swap. Starting with iOS 7, the kernel uses in-memory compression for some pages (similar to zRAM on Android but much more limited). Compressed memory reduces the physical footprint of cold pages but does not eliminate the no-swap constraint. Compressed pages still consume physical RAM, just less of it. When even compressed memory cannot bring the footprint low enough, Jetsam kills.
-
Metal and Core ML models share the Jetsam budget. Apps running machine learning inference with Core ML or rendering with Metal allocate GPU-visible buffers in unified memory. A Core ML model with 500MB of weights loaded into GPU-accessible memory counts against the process footprint. Apps that load multiple models simultaneously (e.g., an AR app running object detection and depth estimation) can silently exceed the Jetsam limit. The fix is to load models lazily and unload inactive ones.
-
Foreground app transitions are not instant. When a user switches apps, the old foreground app does not drop to the suspended band immediately. There is a transition period where it stays at an elevated priority (foreground suspended, band ~600) for roughly 10 seconds. If memory pressure hits during this window, the recently-backgrounded app survives longer than a fully suspended app. This is why "double-tap home and swipe away" feels responsive: the app you just left is still high-priority.
Common mistakes and misconceptions
| Mistake | What they say | Why it is wrong | What to say instead |
|---|---|---|---|
| LRU assumption | "iOS kills the oldest app" | Ignores memory footprint. A 30MB old app should survive over a 1.2GB recent app | "Jetsam sorts by priority band, then by RSS within each band" |
| Swap confusion | "iOS pages memory to disk" | iOS has no swap partition. All process memory is in physical RAM | "iOS has no swap. The only way to reclaim memory is to kill processes" |
| Graceful shutdown | "The app gets a callback to save state before being killed" | Jetsam sends SIGKILL, which cannot be caught. No cleanup opportunity | "SIGKILL is instant. Apps must save state when entering background, not when dying" |
| Foreground immunity | "The foreground app is never killed" | At extreme pressure, even the foreground app can be killed, which the user sees as a crash | "The foreground app has the highest priority but is not immune" |
| Android equivalence | "It works the same as Android" | Android uses oom_adj scores and a different kill heuristic, plus Android has zRAM swap | "Android uses LMK/LMKD with oom_adj scores, which is a different mechanism" |
| Crash report reliance | "Our crash rate is zero, so memory is not a problem" | Jetsam kills produce JetsamEvent logs, not crash reports. Most tools miss them | "Check JetsamEvent logs and MetricKit memory terminations alongside crash reports" |
Practical checklist
- Save user state when entering the background; a process killed by SIGKILL cannot perform cleanup.
- Track physical footprint, not just language-level heap allocations; include mapped files, graphics, and model buffers appropriately.
- Release recreatable caches on memory warnings and keep an internal budget below observed device limits.
- Test foreground, background, extension, split-view, audio, location, and memory-intensive workloads on representative hardware.
- Prefer file-backed or streamed data where appropriate, while understanding that mapping does not make every allocation free.
- Detect memory terminations with device diagnostics and MetricKit in addition to ordinary crash reports.
- Treat priority bands and per-process limits as OS/version-dependent policy, not constants to hard-code.
- Reproduce launch/restore behavior after termination so the app never assumes suspended memory is durable.
Test Your Understanding
Quick Recap
- iOS has no swap space, so the only way to free memory is killing processes.
- Jetsam assigns every process a priority band from 0 (idle daemons) to 1000 (foreground active).
- When memory is critical, Jetsam kills from the lowest band first, targeting the largest process in each band.
- Apps receive
didReceiveMemoryWarningat the warning level, but SIGKILL at the critical level is instant and uncatchable. - Clean memory (file-backed via
mmap) can be reclaimed by the kernel without killing the process, while dirty memory (heap) requires a kill. - App extensions have separate, much smaller memory limits (100-120MB) and are killed independently.
- Smart apps monitor their own memory budget proactively using
os_proc_available_memory()rather than waiting for system warnings. - Android handles this differently using LMK/LMKD with oom_adj scores and zRAM compressed swap, making it less aggressive about killing.
- GPU textures and Core ML model weights count toward the Jetsam footprint on Apple Silicon's unified memory architecture.
- Jetsam kills do not generate standard crash reports. Use JetsamEvent logs and MetricKit to detect memory terminations in production.
Related Concepts
- How garbage collection works: GC reclaims unused objects within a process, but Jetsam operates at the process level, deciding which entire process to kill. GC reduces dirty memory footprint; Jetsam eliminates the process when the footprint is still too high.
- How Linux containers work: Containers use cgroups to enforce memory limits per container, similar to how Jetsam enforces per-process limits. The OOM killer in Linux is the closest analog to Jetsam. Both kill processes when memory exceeds a threshold, both are non-negotiable (SIGKILL), and both decide which process to kill based on a priority scheme.
- How graceful degradation works: Apps responding to memory warnings is a form of graceful degradation, trading functionality (cached images, undo history) for survival. The tiered cache strategy (L1/L2/L3) mirrors how distributed systems shed load under pressure.
- How push notifications work: Push notifications arrive even after an app is killed by Jetsam, because they are handled by a system daemon (apsd), not the app process itself. This decoupling is a deliberate architectural choice: user-facing notifications must survive process termination.
- How virtual memory and paging work: On desktop operating systems, pages are evicted to disk when RAM is full. iOS breaks this assumption entirely. Understanding the contrast helps you explain why iOS must kill processes while macOS and Linux can page them out.
- How mobile app lifecycle works: The iOS app lifecycle (active, inactive, background, suspended, not running) directly maps to Jetsam priority bands. Each lifecycle state determines the app's vulnerability to memory pressure termination.