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 Problem Statement
Interviewer: "You have 20 apps open on your iPhone. The phone only has 6GB of RAM, and the foreground app just requested a large allocation. Walk me through how iOS decides which background app to kill to free up memory."
This question tests three things: your understanding of operating system memory management at the mobile tier, your ability to reason about priority scheduling under resource contention, and whether you know the real constraints of a system with no swap space.
Most candidates say "it kills the oldest app." That is wrong. iOS uses a sophisticated priority band system combined with memory footprint tracking to make targeted kills that minimize user disruption. The difference between "oldest" and "highest memory in the lowest priority band" is the difference between a junior and senior answer.
I like this question because it crosses the boundary between OS internals and user experience. The interviewer is not just testing whether you know the mechanism. They want to see if you understand why the mechanism exists: to maintain the illusion that every app is always running, even on a device with limited RAM and zero swap.
Clarifying the Scenario
You: "Before I answer, I want to make sure I am scoping this correctly."
You: "When you say 'decides which app to kill,' are we talking about the kernel-level mechanism (Jetsam/memorystatus), or the higher-level app lifecycle management (backgrounding, suspension)?"
Interviewer: "Both. Start with the high-level lifecycle, then go into the kill decision."
You: "Got it. Should I also cover what apps can do to survive memory pressure? For example, responding to memory warnings."
Interviewer: "Yes, that is important. I want to understand both sides: how iOS picks the victim and how the app can defend itself."
You: "One more thing. Should I compare this to how Android handles the same problem? Android has a Low Memory Killer (LMK) that works differently."
Interviewer: "Briefly, yes. But focus on iOS."
You: "OK. I will structure this in four parts: the app lifecycle states that determine eligibility, the Jetsam priority band system, the actual kill algorithm, and what apps can do to reduce their memory footprint when warned."
My Approach
I break this into five parts:
- App lifecycle states: How an app moves from foreground to suspended to killed, and what each state means for memory
- Jetsam priority bands: The tiered system iOS uses to categorize every process by importance
- The kill algorithm: How Jetsam selects which process to terminate when memory is critical
- Memory warnings and app response: How apps receive
didReceiveMemoryWarningand what they should do - Comparison with Android: How Android's LMKD takes a different approach to the same problem
The key insight: iOS has no swap space. On a desktop OS, when RAM fills up, the kernel pages cold memory out to disk. On iOS, there is no paging. Every byte of process memory must be in physical RAM or it does not exist. This single constraint drives the entire design. Without swap, the only way to reclaim memory is to kill processes.
This makes the kill decision high stakes. Kill the wrong process, and the user notices immediately when they switch back to an app and it has to cold-launch. Kill the right process, and the user never notices because they were not going back to it anyway.
The mental model I use: think of Jetsam as a bouncer at a nightclub with a strict fire code capacity. When the club hits capacity, the bouncer does not randomly throw people out. They start with the people who have been standing in the corner doing nothing (idle daemons), then move to the people who are just watching (suspended apps), then the people who are chatting quietly (background tasks). The person on stage performing (foreground app) is the absolute last to go. And among the standing-in-the-corner group, the bouncer ejects the person taking up the most space first.
Let me put some concrete numbers on this. A modern iPhone 15 Pro has 8GB of RAM. Here is roughly how that breaks down:
| Consumer | Typical usage | Notes |
|---|---|---|
| XNU kernel + system daemons | ~1.5-2 GB | Always resident, non-negotiable |
| GPU framebuffers + compositing | ~500 MB - 1 GB | Unified memory architecture |
| Foreground app | 500 MB - 3 GB | Varies wildly by app type |
| Background apps (audio, location) | 100 - 500 MB total | Per-app limits enforced |
| Suspended apps | 0 - 3 GB total | First to be killed |
| Free / available | Ideally > 200 MB | Jetsam panics below ~80 MB |
iOS devices have no swap partition. Unlike macOS or Linux, there is no disk-backed virtual memory. When physical RAM fills up, the only option is to terminate processes. This is why iOS is so aggressive about memory management compared to desktop operating systems.
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.
The Tricky Parts
-
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.
What Most People Get Wrong
| 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" |
How I Would Communicate This in an Interview
Here is how I would actually say this:
"iOS manages memory pressure through a kernel daemon called Jetsam, also known as memorystatus. The key constraint is that iOS has no swap space, so every byte of process memory must be in physical RAM. When memory runs low, the only option is to kill processes.
Jetsam organizes every process into priority bands. The foreground app gets the highest band (around 800). Suspended apps sit in a low band (around 100). Background tasks like audio playback sit in between.
When memory pressure hits a critical threshold, Jetsam starts at the lowest priority band and kills the process with the largest physical footprint. If that is not enough, it kills the next largest in the same band, then moves up to the next band. The foreground app is only killed as an absolute last resort.
There is a graduated response before killing starts. First, the system sends memory warnings to the foreground app, giving it a chance to release caches and reduce its footprint. Smart apps monitor their own memory usage proactively using os_proc_available_memory and start evicting caches before the system even asks.
The critical distinction is between clean and dirty memory. Clean memory, backed by files via mmap, can be silently reclaimed by the kernel without killing the process. Dirty memory from heap allocations requires a process kill to reclaim. Apps that push large data into memory-mapped files survive much longer under pressure."
Notice the structure: I start with the constraint (no swap), then the mechanism (Jetsam + priority bands), then the algorithm (lowest band, largest footprint), then the app-side strategy (clean vs dirty memory). This covers the full stack in about 60 seconds. If the interviewer wants more depth on any part, I can go deeper.
The key thing most candidates miss is the clean vs dirty memory distinction. If you mention mmap and explain why file-backed memory is reclaimable without a kill, you have demonstrated deeper understanding than 90% of candidates.
When practicing this answer, time yourself. If your verbal response exceeds 90 seconds on the overview, you are going too deep. Give the overview first, then offer: "Would you like me to go deeper on the priority bands, the kill algorithm, or how apps can survive pressure?" Let the interviewer guide the deep dive.
Interview Cheat Sheet
- Trigger: "How does mobile OS manage memory?" Say: "iOS has no swap, so the only option is killing processes. Jetsam daemon manages this with priority bands."
- Trigger: "What order are apps killed?" Say: "Lowest priority band first, then largest RSS within that band. Not LRU, not random, not oldest."
- Trigger: "What are the priority bands?" Say: "Foreground active (~800), foreground suspended (~600), background active (~400), suspended (~100), idle daemons (~0)."
- Trigger: "Can apps avoid being killed?" Say: "Yes. Respond to memory warnings, reduce dirty footprint, use mmap for large read-only data, and monitor your own memory budget proactively."
- Trigger: "Clean vs dirty memory?" Say: "Clean memory is file-backed and can be silently reclaimed. Dirty memory is heap-allocated and requires a process kill to reclaim."
- Trigger: "What about memory warnings?" Say: "didReceiveMemoryWarning fires at the warning level. But SIGKILL at critical level is instant, no callback, no cleanup."
- Trigger: "How does Android differ?" Say: "Android uses LMK/LMKD with oom_adj scores instead of Jetsam bands, and Android has zRAM compressed swap, so it is less aggressive about killing."
- Trigger: "What about extensions?" Say: "App extensions get their own much smaller memory limits, typically 100-120MB. They are separate processes with separate Jetsam limits."
- Trigger: "GPU memory?" Say: "On Apple Silicon with unified memory, GPU textures count toward the process footprint. Games with large textures can exceed the Jetsam limit even with small heap usage."
- Trigger: "Per-process limits?" Say: "Apple enforces a per-process Jetsam limit that varies by device. iPhone 15 Pro allows roughly 3-4GB for a foreground app. iPhone SE allows about 1-1.2GB. These limits are not documented publicly."
- Trigger: "How do I debug memory issues?" Say: "Use Instruments' Allocations and VM Tracker tools, check for JetsamEvent logs in Console, and integrate MetricKit to capture memory terminations in production."
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.