A health-tech client came to us with a mystery that was tanking their App Store rating.
Users with iPhone 11 and iPhone 12 devices were furious. They would open the app, start filling out a 10-step medical intake form, switch to Apple Wallet or their camera to snap an insurance card, switch back—and the app would restart from scratch. All form data lost.
The engineering team looked at Sentry and Firebase Crashlytics. Zero crashes reported. Their crash-free session metric proudly displayed 99.9%.
“It must be an iOS bug,” they told the product manager.
It wasn’t an iOS bug. It was Jetsam.
How iOS Silently Murders Your App
iOS doesn’t have a disk swap file like macOS or Windows. When physical RAM runs low, the Darwin kernel has two options: compress memory pages, or terminate processes.
The kernel subsystem responsible for this is called Jetsam (memorystatus). It maintains a strict priority queue of running processes:
[Foreground App] ── High Priority
│
[Audio / GPS Background Tasks] ── Medium Priority
│
[Suspended Background Apps] ── Lowest Priority (First to be killed)
When your user switches away from your app to open the camera, the iOS Camera app requires up to 700MB of RAM to run real-time neural network filters and RAW image processing.
The kernel scans suspended apps. It looks at Dirty Memory—memory that has been written to and cannot be purged from disk. If your app is holding onto 280MB of dirty memory because of an uncompressed bitmap cache or un-garbage-collected JavaScript context, the kernel issues a SIGKILL (kill -9).
No exception is thrown. No signal handler gets invoked. Your crash reporting SDK never gets a chance to write a crash log. To Crashlytics, your app simply closed normally.
To the user, your app just crashed and erased their intake form.
Diagnosing Jetsam with Xcode and IPS Logs
You will never catch Jetsam issues in the iOS Simulator on an Apple Silicon Mac with 64GB of unified memory. You must test on a physical low-memory device (an iPhone SE 2nd Gen or iPhone 11 is our studio gold standard).
Connect the device to a Mac and navigate to: Settings > Privacy & Security > Analytics & Improvements > Analytics Data
Search for logs starting with JetsamEvent-.... Open one:
{
"timestamp": "2026-01-28 14:12:05.10 -0500",
"bug_type": "298",
"os_version": "iPhone OS 18.2",
"incident_id": "89A42C18-...",
"pageSize": 16384,
"states": ["frontmost", "background"],
"processes": [
{
"uuid": "4391A...",
"procname": "ClientMobileApp",
"reason": "per-process-limit",
"rpages": 19482, // 19,482 pages * 16KB = ~311 MB dirty footprint!
"jetsam_level": 0
}
]
}
There is your proof: reason: per-process-limit. The kernel executed your process because it exceeded its allocated memory ceiling while backgrounded.
The Tri-Fold Remedy
- Evict Image Caches on Backgrounding:
In your native
AppDelegateor React NativeAppStatelistener, flush all in-memory image decoders the moment the app enters the background:
func applicationDidEnterBackground(_ application: UIApplication) {
// Drop all decoded in-memory CGImage buffers
ImagePipeline.shared.configuration.imageCache?.removeAll()
URLCache.shared.removeAllCachedResponses()
}
- Never Hold High-Res Bitmaps in Views:
If an avatar displays at 48x48 points on a 3x Retina screen, it needs a 144x144 pixel buffer (~82KB). If you load the user’s raw 12-megapixel photo directly into an
UIImageView, iOS allocates a 48MB uncompressed bitmap buffer in memory. - Persist Form State Incrementally: Never rely on RAM to keep state across app switches. Write every keystroke into an encrypted SQLite or MMKV scratch buffer so that when the app restarts after a Jetsam event, it resumes on the exact same field seamlessly.
Fixing this one issue restored our client’s App Store rating from 2.9 to 4.7 in eight weeks.