Claude Code on iOS: spotted the overload, botched the report
Several Claude Code agents ran in parallel on my iOS app and maxed out a 12-core Mac. One spotted it in a crash report. Its summary had three false claims. →
Software engineer · Málaga

On this page
Short answer: On Sunday I had several Claude Code agents working on Rydger, my iOS app, and my 12-core Mac hit a load average of 422. One agent read a single line in a crash report and saw right away that the machine was saturated. Its explanation, never fully confirmed: too many simulator boots and builds at once. Then the same agent wrote me a summary with three false claims, and I came close to publishing it as this post.
It started with a crash report from MobileCal, the iOS Calendar app, running inside the simulator. When we counted later, the reports folder held 141 of them from the past two days. I pasted the MobileCal one into an agent and asked what was going on.
The summary I got back later even suggested a title (in Spanish, translated here): “A load of 422 and 74 zombie processes.” There were no zombies.
One line in the MobileCal report showed the CPU was maxed out
The report was a 0x8BADF00D, the iOS watchdog’s code, which Apple says is pronounced “ate bad food.” MobileCal had 30 seconds to launch and ran out of time.
The useful part was near the top, in the termination reason, under WatchdogCPUStatistics:
"Elapsed total CPU time (seconds): 360.720 (user 292.190, system 68.530), 100% CPU",
"Elapsed application CPU time (seconds): 1.802, 0% CPU"
In those 30 seconds of wall-clock time, MobileCal got 1.8 seconds of CPU while the system as a whole used 360. According to Apple’s docs, the total covers every process on every core, and a low number for the app means it was mostly idle, waiting on system resources. 360 CPU seconds in 30 wall-clock seconds works out to almost 12 cores busy the entire time, which is every core in my M2 Max. The thread running when the watchdog fired was still in dyld_sim, loading libraries. MobileCal never got as far as main.
The agent spotted that and went straight to the machine instead of the code. Its first command line included uptime and a list of the booted simulators:
11:53 up 13 days, 2:17, 2 users, load averages: 422.71 304.75 191.54
Three simulators were booted, at least two agents were working, and a unit test suite was building in the background. Three of the top four CPU consumers belonged to the simulator runtime and had been running for only 2 to 4 seconds; the fourth was ANECompilerService. By the agent’s own account, it had settled on an explanation about five minutes later: too many simulator boots and builds at once for 12 cores. That’s the most likely story, but I didn’t measure how much each part contributed.
Next time simulator crash reports pile up, I’ll open the 0x8BADF00D ones first and compare the app’s CPU time to the total. If the total gets close to wall-clock time multiplied by the core count, the problem is the Mac. That’s one case, one report out of 141, so I treat it as a first check, not a rule.
The other 140 reports didn’t point at the cause
Another agent recounted them on the Mac that afternoon: 139 of the 141 came from simulators, spread across 20 different simulators.
- 94
Trace/BPT trap: 5from simulator daemons such asintelligencetasksd(38 of them) andAppIntentsLiveEntityService. They kept showing up after the load came down, so overload isn’t required to trigger them. Nobody has looked into why they crash yet. In chat, the agent blamed theintelligencetasksdones on an iOS 27 runtime bug without checking. - 44
XPC_EXIT_REASON_SIGTERM_TIMEOUT: daemons that don’t exit in time when a simulator shuts down. I’d guess there are more of them under heavy load, but I haven’t measured it. - A single
0x8BADF00D, the MobileCal one. - Two from the Mac itself: an
upload-symbolsbus error nobody has analyzed, and one from a terminal multiplexer.
The screenshot script rebooted the simulator for every language
In my last post on subagents I wrote that every worktree needs its own simulator. The agent capturing App Store screenshots was in its own worktree, but it was using the main checkout’s simulator.
Its script, capture.sh, did this for each of the 12 languages: shut down the simulator, boot it, and run xcodebuild test, which runs an incremental build step on every call. A comment explained why: back-to-back UI test runs without a reboot failed to launch the test runner with “Application failed preflight checks” (Busy). The language, though, already reached the app as a launch argument (-AppleLanguages), so no language ever needed its own build.
The agent changed it to build once with build-for-testing, run each language with test-without-building, and reboot only before a retry, in case the Busy error comes back. The part that matters is still unchecked: nobody has run a full 12-language batch on the new script while watching the load, and there are no timings for the old one. And another problem is still open: capping how many simulators boot at once on the Mac.
The agent’s summary had three false claims
Along the way, the agent found 74 Crashlytics upload-symbols processes. Firebase’s run script forks one into the background with & on nearly every build. The oldest had been alive for 10 days. All true so far. The summary it gave me in chat also said:
- They were zombie processes. They were sleeping (state S) at 0% CPU, with their parent still alive, so they were simply hung. And a process that uses no CPU doesn’t add to the load.
- After the fix, the load dropped to 3.6. It dropped on its own. It spiked to 148 at 12:03 when the agent reran the full test suite, then was down to around 8 by 12:09, about 11 minutes before anyone killed those processes. It came down on its own as the simulator boots and builds wound down.
- They were waiting for a dSYM that never shows up in Debug. Rydger’s Debug builds do produce a dSYM. I still don’t know why they hung.
All three match the story you’d expect: processes piling up sound like zombies, and a load drop right after a fix looks like the fix worked. Those sentences were going to be the title and the takeaways of this post. Another takeaway said the StoreKit tests failed at load 400 and passed at load 10. They failed in one run out of three, and one of the passing runs was a full suite with the load at 148, measured right as it finished.
Twenty-three questions got the agent to retract all three claims
Before writing, I sent the agent 23 questions and one instruction: paste the literal output of every command, and if something wasn’t measured, say so instead of estimating. It came back with a document that opened (in Spanish) with “Corrections before we start” and retracted all three. It also fixed a comment it had left in Rydger’s code repeating the dSYM story.
This is a different problem from the one in my AI agent verifier post. There, a second agent checks what’s said about the code against the code, which is still there tomorrow. A machine’s state isn’t. That 422 load average can’t be measured again, and there’s no Activity Monitor screenshot. If nobody saves the uptime and ps output while the incident is live, the agent’s summary becomes the only record.
Even the corrected report miscounted
The corrected document listed 92 SIGTRAP reports (84 + 8) and 41 SIGTERM timeouts (27 + 14). When another agent recounted them, with a loop that reads the indicator field in each .ips file, it got 94 and 44. The gap is small, but it says something: the questions made the document more honest about what wasn’t measured, not more accurate about what was. And I got lucky: I only asked those questions because I was writing a post.
Save the output before you ask for the summary
While the incident is still live, save the output to a file in the repo. It’s the one part you can’t rerun. This is enough:
{ date; uptime; xcrun simctl list devices booted;
ps -Ao pid,ppid,stat,pcpu,etime,command -r | head -40;
ls -lt ~/Library/Logs/DiagnosticReports | head -50; } > incident-$(date +%F-%H%M).txt
Then, when the agent hands you its summary, send this:
For every number and every cause in your summary, cite the line in the
evidence file that proves it. Mark anything that isn't there or that you inferred.
Before you accept it, count one number with your own command, or have an agent that didn’t write the summary do it. The second agent’s recount didn’t match.
Better still: let the runner save it
On LinkedIn, Roberto Gómez, an iOS developer, pointed out the weak spot in the command above: it depends on someone remembering to run it while the incident is still live. Put it in a skill for the agent and the agent can skip it or get there too late. The suggestion was to take the capture away from the agent and put it in the script that launches the builds: a snapshot of the machine before starting and another whenever something fails. That way the only evidence doesn’t depend on the same agent that later explains what happened.
It’s the better version, and I’m applying it to Rydger and WealthSim. The idea fits in a fifteen-line wrapper:
#!/bin/bash
# Snapshot before the run, and again if it fails or gets killed
snap() {
mkdir -p .incidents
{ echo "# $1 $(date '+%F %T') ${2:+exit=$2}"
uptime; sysctl -n hw.ncpu
xcrun simctl list devices booted
ps -Ao pid,ppid,stat,pcpu,etime,command -r | head -40
ls -lt ~/Library/Logs/DiagnosticReports | head -50
} > ".incidents/$(date +%Y%m%d-%H%M%S)-$1.txt" 2>&1
}
snap start
"$@" & child=$!
trap 'kill -TERM $child; snap killed TERM; exit 143' TERM INT
wait $child; code=$?
[ $code -ne 0 ] && snap fail $code
exit $code
Put it in front of your usual command: ./run-with-capture.sh xcodebuild test -scheme MyApp -destination "id=$udid", and add .incidents/ to .gitignore. Three details matter:
- It returns the command’s own exit code, so whatever depends on it keeps working. I tested it with a run that passes, one that fails with 3 and one I killed halfway: it returned 0, 3 and 143.
- The failure snapshot comes before any cleanup or retry. If your script shuts the simulator down or retries the test, the snapshot has to happen first, or you’ll never see what was there.
- It also captures when the run gets killed. A timeout or a Ctrl-C is exactly when the machine was at its worst.
A fuller version should list only the crash reports newer than the start of the run, and prune old snapshots so the folder doesn’t grow forever.
None of this prevents the overload. For that, Roberto suggests starting with one build or test run at a time through a shared runner, while the other agents keep reading and editing code, and raising the limit only after measuring. What the capture changes is that when you ask the agent for its summary, the evidence file already exists and nobody had to remember to save it.
Sources
- Apple: Addressing watchdog terminations, read November 1, 2026.
- My evidence: the
.ipsfiles in~/Library/Logs/DiagnosticReports/, the agent’suptimeandpsoutput, and Rydger’s scripts. The repo is private.
Related reading: Claude Code skills vs subagents and the shared simulator and the verifier that sent back four builds out of five.
P.S. Has an agent ever handed you a convincing summary that fell apart once you asked for proof? Tell me on LinkedIn.
Last updated:


