◉RORK LABJP
●EXPO — EAS Observe now records native crashes (Oct 7). On SDK 57, update to 57.0.21 or later●SDK 58 — SDK 58 Beta has been out since Sep 15. The stable date is still unconfirmed●RN 0.88 — React Native 0.88.x is scheduled for Oct 12, 3 days left●Q&A — People are asking why expo-widgets render blank only in production builds●RORK — GPT-6.1 Sol was added on Sep 29, available on Pro and Max plans●NEW — Building an app for a client? Decide who owns the publishing account first●EXPO — EAS Observe now records native crashes (Oct 7). On SDK 57, update to 57.0.21 or later●SDK 58 — SDK 58 Beta has been out since Sep 15. The stable date is still unconfirmed●RN 0.88 — React Native 0.88.x is scheduled for Oct 12, 3 days left●Q&A — People are asking why expo-widgets render blank only in production builds●RORK — GPT-6.1 Sol was added on Sep 29, available on Pro and Max plans●NEW — Building an app for a client? Decide who owns the publishing account first
Articles/Dev Tools
⬡ Dev Tools/2026-07-10Advanced

The Termination That Never Shows Up as a Crash — Reading JetsamEvent in Rork Apps

Crashlytics is silent, yet reviewers write that the app closes by itself. Most of the time the OS killed it for exceeding its memory limit. Here is how to read JetsamEvent reports and design an image-heavy app's memory budget from measured values.

Rork577Rork Max235Memory5iOS115Indie Development26

✦ Premium Article

For a while, one of my wallpaper apps collected App Store reviews saying the app closed on its own after a minute or two, while the Crashlytics dashboard stayed completely quiet. The only reproduction step anyone offered was "scroll a lot." Nothing happened on my own device, no matter how long I scrolled.

The answer showed up when I opened Settings, then Privacy & Security, then Analytics & Improvements, then Analytics Data on a device, and found a stack of files named JetsamEvent-2026-…. The app had not crashed. iOS had killed it for using too much memory, which is why nothing ever reached the crash reporter.

Apps built with Rork or Rork Max sit in exactly the same position. However tidy the generated code is, the memory ceiling iOS grants a process does not move. If anything, generated code tends to be generous with images and lists, which makes that ceiling easier to touch.

A Jetsam kill is not a crash

iOS runs a mechanism called Jetsam. When system-wide memory gets tight, or when a single process exceeds its per-device limit, the kernel sends that process a SIGKILL and reclaims it.

SIGKILL cannot be handled. That is the crux of the problem. Crashlytics and Sentry install signal and exception handlers to catch failures. A Jetsam kill gives them no opportunity to run, so nothing is recorded — and nothing appears under Crashes in App Store Connect either.

Instead, these terminations land in MetricKit as MXAppExitMetric, specifically cumulativeMemoryResourceLimitExitCount. Plenty of indie developers never look there. I did not either.

Type of terminationReaches the crash reporter?Where to look
Exceptions and signals (SIGSEGV, etc.)YesCrashlytics / Organizer > Crashes
Memory limit exceeded (Jetsam)NoOn-device Analytics Data / MetricKit
Watchdog (slow launch)SometimesOrganizer > Hangs, 0x8badf00d
Reclaimed while backgroundedNoNormal behavior. No action needed

That last row matters more than it looks. I wrote up the staged-release approach itself in a five-tier memory pressure release architecture for iOS. Reclaiming a backgrounded app is ordinary iOS behavior. Trying to eliminate every JetsamEvent will sink you into pointless work. The ones worth chasing are the kills that happened in the foreground.

Reading a JetsamEvent report

Pull the file out of Analytics Data on the device — the share button will hand it to AirDrop or Files. The contents are JSON-flavored text.

Start with the header near the top.

{"bug_type":"298","timestamp":"2026-06-14 21:03:11.42 +0900","os_version":"iPhone OS 26.1 (23B82)",
 "incident_id":"…","pageSize":16384,"memoryStatus":{"compressorSize":98213,"memoryPages":{"active":41022,
 "free":1290,"wired":38310}}}

A bug_type of 298 means Jetsam. Note pageSize: 16384, or 16 KB. Assuming 4096 throws every later conversion off by a factor of four. Devices from the A14 onward use 16 KB pages, so always read this field rather than hardcoding it.

Then find the victim. The body is an array of process states, each shaped like this.

{"uuid":"…","states":["frontmost"],"killDelta":0,"genCount":0,"age":133,"purgeable":0,
 "fds":124,"coalition":312,"rpages":86214,"reason":"per-process-limit",
 "name":"WallpaperApp","cpuTime":41.2,"idleDelta":0}

Three fields carry the signal.

  1. Whether states contains frontmost — if it does, the app vanished while the user was looking at it
  2. reason — per-process-limit means your app blew its own ceiling; vm-pageshortage means it got caught in a system-wide squeeze
  3. rpages — resident pages. Convert with rpages × pageSize ÷ 1024 ÷ 1024

In this example, 86214 × 16384 ÷ 1048576 ≈ 1,347 MB. The app hit the roughly 1,384 MB limit on an iPhone SE (3rd generation) and was killed.

Doing that by hand gets old, so here is a script.

#!/usr/bin/env bash
# jetsam-summary.sh — summarize .ips files pulled from Analytics Data
# usage: ./jetsam-summary.sh ~/Downloads/JetsamEvent-*.ips
for f in "$@"; do
  python3 - "$f" <<'PY'
import json, sys, re
raw = open(sys.argv[1], encoding="utf-8").read()
# the header line and the body are concatenated; split on the first newline
head, body = raw.split("\n", 1) if "\n" in raw else (raw, "{}")
page = json.loads(head).get("pageSize", 4096)
ts   = json.loads(head).get("timestamp", "?")
for m in re.finditer(r'\{"uuid".*?\}', body):
    p = json.loads(m.group(0))
    mb = p.get("rpages", 0) * page / 1048576
    if p.get("reason") in ("per-process-limit", "vm-pageshortage") and mb > 100:
        front = "FRONTMOST" if "frontmost" in p.get("states", []) else "background"
        print(f'{ts}  {p["name"]:<22} {mb:8.1f} MB  {p["reason"]:<18} {front}')
PY
done

I ran it across 41 reports covering three weeks. Twelve were foreground per-process-limit kills, and eleven of those happened on the same screen — the wallpaper grid. From there it stopped being a mystery and became an implementation problem.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦How to convert rpages in a JetsamEvent report into MB and identify which process died at what size
✦Measured limits from os_proc_available_memory(): 1,384 MB on an iPhone SE versus 2,099 MB on an iPhone 13
✦Swift and React Native implementations that pin the image cache to 25% of the remaining memory budget
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $15 for lifetime access
View Membership →

Related Articles

⬡ Dev Tools2026-09-21
A build that stops with exit code 0 — the flag that silenced the log, and the check that let an empty file through
Two reports landed in the same week: eas build --local stopping with exit code 0 but produced no further output, and a zero-byte stub that never got embedded. Both came down to settings that reduce output. Here is how to audit your own scripts.
⬡ Dev Tools2026-09-05
Your Widget Extension Can Submit a BGTaskScheduler Request. It Just Cannot Register the Handler
Calling BGTaskScheduler from a widget extension compiles, submits, and returns true — and then nothing runs. Here is why registration belongs to the host app only, and how I moved refresh ownership back where it belongs across my wallpaper apps.
⬡ Dev Tools2026-07-27
When to Raise Your Minimum iOS Version — Count Leftover Branches, Not User Percentages
Judging a minimum OS bump by usage share produces the same answer every year, so the decision never happens. Here is the annotation convention, the sweep script that counts how many iOS and Android branches each candidate floor would retire, and what to watch for 30 days after.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links