Engineering notes

On iOS, run background audio processing on the Neural Engine

November 6, 2026 · Liner Notes · 日本語版

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.

MechanismHow long it can runMeant for
beginBackgroundTaskAbout 30 secondsFinishing work right before the app leaves the screen
BGProcessingTaskThe system picks the timeWork that can wait (overnight while charging, for example)
BGContinuedProcessingTask (iOS 26+)As long as it keeps reporting progressLong work the user started; progress is shown on screen
Audio background modeWhile recording or playingRecording 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 unitsForegroundBackground (continued processing)Background (while recording)
Neural Engine2.2 s4.3 s (2×)2.4 s
CPU only8.4 s102–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

Neural EngineCPU 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
Show as a table
Neural EngineCPU only
Foreground2.2 s8.4 s
Background (continued processing)4.3 s102–174 s
Background (while recording)2.4 s7.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 jobElapsed timeCPU time usedEnergy (foreground = 1)
Foreground8.7 s7.9 s1
Background112 s86 s0.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

ElapsedCPU time
Foreground
8.7 s
7.9 s
Background
112 s
86 s
Show as a table
ElapsedCPU time
Foreground8.7 s7.9 s
Background112 s86 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 chunkCPU only
Foreground0.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

Neural EngineCPU only
userInitiated
2.2 s
8.4 s
utility
2.8 s
7.6 s
background
7.9 s
8.3 s
Show as a table
Neural EngineCPU only
userInitiated2.2 s8.4 s
utility2.8 s7.6 s
background7.9 s8.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.

WorkBefore (after the recording stopped)Now (while recording)
TranscriptionThe whole recording at onceEvery 12 seconds while recording
Voice separationRe-streaming the recording (over 5 minutes in the background)Done while recording; 0.3 s after stopping
Laughter detection167 s for a 29-minute recordingDone while recording; 0 s after stopping

Time after the recording stops (laughter detection, 29-minute recording, background)

Seconds

Before (detect after recording)
167 s
Now (use results from the recording)
0 s
Show as a table
ItemValue
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

The measurement app and its data are on GitHub (yamannyama/granite-speech-coreml).

More engineering notes