I tried to process a recorded conversation while my iPhone app was in the background. A step that takes 33 seconds in the foreground had not finished after 5 minutes, and then the app disappeared without leaving any record. Three problems turned out to be stacked on top of each other. The biggest one: in the background, only work that runs on the CPU slows down. Inference on the Neural Engine barely slows down at all. The same job was 12–21× slower on the CPU and within 2× on the Neural Engine. If you process audio in the background, put as much of it as you can on the Neural Engine. Here is how I found that out and what I changed.
What I wanted to do
I build Liner Notes, an iPhone diary app that turns conversations into text on the device. After a recording stops, an "ingest" step separates speakers, transcribes, and summarizes. For a 44-minute recording it takes about 4 minutes in the foreground. People switch to another app as soon as they stop recording, so I wanted this step to finish in the background.
The kinds of background execution on iOS
On iOS, how long an app may run in the background depends on the mechanism.
| Mechanism | How long it can run | Meant for |
|---|---|---|
| beginBackgroundTask | About 30 seconds | Finishing work right before the app leaves the screen |
| BGProcessingTask | The system picks the time | Work that can wait (overnight while charging, for example) |
| BGContinuedProcessingTask (iOS 26+) | As long as it keeps reporting progress | Long work the user started; progress is shown on screen |
| Audio background mode | While recording or playing | Recording and music apps |
The ingest is long work that starts when the user stops recording, so I used BGContinuedProcessingTask.
What happened: slow, and then gone without a trace
The "separate voices" step, 33 seconds in the foreground, had not finished after 5 minutes in the background. Eventually the whole process disappeared. No crash log, no expiration callback, no out-of-memory record.
In my logs, 18 of 62 continued-processing requests had not started after 10 minutes, and the ones that did start disappeared after 40–627 seconds. Apple's developer forums have a post about the same thing ("4–5× slower in the background") with no known cause.
What I tried first: rest the CPU, report progress more often
My first guess was that iOS was stopping the app for using too much CPU, so I added pauses between steps. The work got slower and kept disappearing just as often.
Next, the forums said a task is cut off if its progress stalls for about 30 seconds. I added a heartbeat that advanced progress every 5 seconds from off the main thread. The app still disappeared 25 seconds in, and the heartbeat log showed progress had never stalled. The cause was somewhere else.
Cause 1: my own code ignored the deadline
The ingest started with beginBackgroundTask(expirationHandler: nil). Written this way, when the grace period runs out about 30 seconds after the app leaves the screen, the app does not end its own work, and iOS terminates it without a record. The 18 requests that seemed never to start were actually being killed this way, 30–55 seconds in.
Once the app ended its own work when the grace period ran out, the silent disappearances stopped. A 29-minute conversation now finished and saved in 2 minutes 15 seconds entirely in the background. It was still slow, though.
Cause 2: only CPU work was running on slow cores
To find out what was slow, I built a measurement app. It runs the same Core ML job (transcribing 10 minutes of Japanese speech) with different compute units and app states. iPhone 16, iOS 27, median of two runs.
| Compute units | Foreground | Background (continued processing) | Background (while recording) |
|---|---|---|---|
| Neural Engine | 2.2 s | 4.3 s (2×) | 2.4 s |
| CPU only | 8.4 s | 102–174 s (12–21×) | 7.3 s |
Time to transcribe 10 minutes of Japanese speech
Seconds (shorter is faster). CPU-only in the background is drawn at the middle of 102–174 s
Show as a table
| Neural Engine | CPU only | |
|---|---|---|
| Foreground | 2.2 s | 8.4 s |
| Background (continued processing) | 4.3 s | 102–174 s |
| Background (while recording) | 2.4 s | 7.3 s |
Neural Engine inference stays within 2×; only CPU work is 12–21× slower. While recording (with the microphone open), both run at foreground speed even in the background.
To see why the CPU was slow, I read the process's CPU time and energy from task_info.
| CPU-only job | Elapsed time | CPU time used | Energy (foreground = 1) |
|---|---|---|---|
| Foreground | 8.7 s | 7.9 s | 1 |
| Background | 112 s | 86 s | 0.22 |
Elapsed time and CPU time for the CPU-only job
Seconds. Even in the background, the process gets CPU time for almost the whole elapsed time
Show as a table
| Elapsed | CPU time | |
|---|---|---|
| Foreground | 8.7 s | 7.9 s |
| Background | 112 s | 86 s |
Even in the background, the process is given CPU time for most of the elapsed time. Yet it needs 11× the CPU time to finish the same job, at 1/4.6 of the energy. The numbers all fit if the work is not being starved of CPU but is running on the low-power cores at a low clock. I have not looked at core placement in Instruments, so this is an inference.
The audio is processed in chunks of up to 20 seconds, so I also looked at the time per chunk. In the background it is slow from the very first chunk, and sometimes it switches to an even slower stretch partway through.
| Time per chunk | CPU only |
|---|---|
| Foreground | 0.14 s |
| Background (from the first chunk) | 1.7 s |
| Background (slow stretch) | About 8 s |
Apple's forums have a separate report that "even in the foreground, threads are moved to efficiency cores after 30 seconds of sustained load." Here it is slow from the first chunk, so it is a different phenomenon.
One more finding: if the task that calls Core ML has .background priority, Neural Engine inference is 3.6× slower even in the foreground. CPU work did not change with priority.
Time by task priority (foreground)
Seconds
Show as a table
| Neural Engine | CPU only | |
|---|---|---|
| userInitiated | 2.2 s | 8.4 s |
| utility | 2.8 s | 7.6 s |
| background | 7.9 s | 8.3 s |
The "separate voices" step that kept disappearing includes CPU work such as converting audio and cutting it into segments, and I believe that is what slowed down.
Cause 3: sometimes the continued-processing task never starts
Even after fixing 1 and 2, I reproduced a case where a continued-processing request did not start for more than 9 minutes. That is iOS behavior; the app cannot fix it.
What I changed: finish the heavy work while recording
While recording, the app runs at foreground speed even in the background, so I moved the heavy parts of the ingest into the recording itself. After the recording stops, the app only uses those results.
| Work | Before (after the recording stopped) | Now (while recording) |
|---|---|---|
| Transcription | The whole recording at once | Every 12 seconds while recording |
| Voice separation | Re-streaming the recording (over 5 minutes in the background) | Done while recording; 0.3 s after stopping |
| Laughter detection | 167 s for a 29-minute recording | Done while recording; 0 s after stopping |
Time after the recording stops (laughter detection, 29-minute recording, background)
Seconds
Show as a table
| Item | Value |
|---|---|
| Before (detect after recording) | 167 s |
| Now (use results from the recording) | 0 s |
Work that cannot be done during the recording runs on the Neural Engine, with Core ML's compute units set to .cpuAndNeuralEngine. The GPU is not available in the background, so .all gains nothing there.
In case the continued-processing task never starts, the transcript is saved the moment the recording stops, and the summary is added the next time the app opens.
The only heavy work left after the recording stops is re-sorting speakers. For a 10-minute recording it took 28 s in the background: 8 s for segmentation and embeddings, 0.5 s for CPU clustering, and the rest loading models.
Summary
- If you process audio in the background, put as much of it as you can on the Neural Engine. The same job was within 2× on the Neural Engine and 12–21× slower on the CPU
- When something is slow in the background, suspect the CPU pre- and post-processing before the model
- With
beginBackgroundTask, end your work when the grace period runs out. If you ignore the deadline, iOS terminates the app without a record - Do not call Core ML from a
.background-priority task. It is 3.6× slower even in the foreground - In a recording app, do the heavy work while recording. With the microphone open, the background runs at foreground speed
- A continued-processing task may never start. Save intermediate results first
The measurement app and its data are on GitHub (yamannyama/granite-speech-coreml).
More engineering notes
- Choosing a speech recognizer by benchmark alone is risky. Measure with the audio your app actually handles
On read speech Apple's recognizer looked best; on conversation its error rate was 1.8x Parakeet's. How to compare Japanese speech recognizers, with data.
- Same transcription accuracy, three times the errors in the summary
Two recognizers with the same CER produced 3x the summary errors. Why a stricter summary prompt made it worse, and how to compare by summary quality.
- Apple's Private Cloud Compute refused 10.6% of my app's requests as unsafe
Apple's Private Cloud Compute (Foundation Models) refused 10.6% of 614 production requests and rate-limited 9.6%. What broke and how I redesigned.
- I built an AI copy of myself following prior research. Whatever I fed it, agreement stopped at 62–67%
Following prior research, I built an AI copy of myself. Diary or one line, agreement stayed at 62-67%. What identifies a person is how they speak.