Home / Blog / AVFoundation audio recording: three pitfalls that only show in the background

AVFoundation audio recording: three pitfalls that only show in the background

I built an iOS voice-note app. AVFoundation’s recording API looks straightforward in the docs. On a real device, background, interruptions, and mix state are a mess. Here are three pitfalls worth avoiding. Pitfall 1: turning background mode on isn’t enough Adding audio to UIBackgroundModes in Info.plist is step one. By itself it’s not enough. For […]

I built an iOS voice-note app. AVFoundation’s recording API looks straightforward in the docs. On a real device, background, interruptions, and mix state are a mess. Here are three pitfalls worth avoiding.

Pitfall 1: turning background mode on isn’t enough

Adding audio to UIBackgroundModes in Info.plist is step one. By itself it’s not enough. For recording to actually continue in the background, you have to configure AVAudioSession correctly.

let session = AVAudioSession.sharedInstance()
try session.setCategory(
    .playAndRecord,
    mode: .default,
    options: [.defaultToSpeaker, .allowBluetoothA2DP]
)
try session.setActive(true)

I pick .playAndRecord over .record. The user might need notification sounds to play through during a recording. With plain .record, mixer scenarios like Bluetooth headsets misbehave.

Important detail: I do this config when the app comes to the foreground, not when recording starts. Coming back from background, you need to flip setActive from false to true or the audio route doesn’t change.

Pitfall 2: interruption handling

When a phone call comes in or another app starts playing audio, recording stops. It doesn’t resume on its own. You have to listen for the interruption notification and react:

NotificationCenter.default.addObserver(
    forName: AVAudioSession.interruptionNotification,
    object: nil,
    queue: .main
) { [weak self] note in
    guard let info = note.userInfo,
          let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
          let type = AVAudioSession.InterruptionType(rawValue: typeValue)
    else { return }

    switch type {
    case .began:
        self?.recorder?.pause()
    case .ended:
        guard let optionsValue = info[AVAudioSessionInterruptionOptionKey] as? UInt else { return }
        let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
        if options.contains(.shouldResume) {
            try? AVAudioSession.sharedInstance().setActive(true)
            self?.recorder?.record()
        }
    @unknown default: break
    }
}

The shouldResume flag doesn’t always come. It comes back after a phone call, but not if the user triggered Siri. In our app we decided to show a “resume?” button and let the user continue manually. The people we’d tried to auto-resume for ended up with half-recorded sessions.

Pitfall 3: background file writes are limited

iOS’s audio background mode works, but I/O performance in the background is different. Writing to a single large file with AVAudioRecorder introduces I/O latency on recordings longer than 40 minutes, and you get occasional frame drops.

My fix: split long recordings into five-minute chunks. Every five minutes I stop the recorder and start a new file. File-system pressure stays low throughout the background session. I concatenate the chunks afterwards:

let composition = AVMutableComposition()
let track = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid)!
var time = CMTime.zero
for url in chunkURLs {
    let asset = AVURLAsset(url: url)
    let assetTrack = try await asset.loadTracks(withMediaType: .audio).first!
    let duration = try await asset.load(.duration)
    try track.insertTimeRange(
        CMTimeRange(start: .zero, duration: duration),
        of: assetTrack,
        at: time
    )
    time = CMTimeAdd(time, duration)
}

The user sees one recording file. I stitch 12 chunks back together behind it.

Power consumption

Background recording is a battery drain. An hour of background recording on an iPhone 14 ate 11% of the battery. You have to show the user a banner (“recording still running”) while it’s happening, otherwise “why did my battery die” complaints roll in. I show it as a Live Activity, and I auto-stop at a user-configured deadline.

Format choice: quality vs file size

The default LPCM format generates huge files. An hour of audio is 550 MB. AAC brings that down to 60 MB:

let settings: [String: Any] = [
    AVFormatIDKey: kAudioFormatMPEG4AAC,
    AVSampleRateKey: 44100,
    AVNumberOfChannelsKey: 1,
    AVEncoderBitRateKey: 96000
]
let recorder = try AVAudioRecorder(url: url, settings: settings)

96 kbps mono is more than enough quality for human speech. Mono is deliberate; stereo doubles the file and makes mixer operations more expensive.

Things I couldn’t test

Most of this code doesn’t behave correctly in the simulator. Interruption and background mode testing has to happen on real hardware. I tested across 15 iPhone models in TestFlight; the worst bugs surfaced on the iPhone SE 2nd gen, where constrained RAM makes background recording trigger aggressive memory warnings.

Final advice

Before shipping a recording feature, use it yourself for two weeks. Most edge cases only show up in daily use. Interruption, background, and low-power mode, in combination, produced six separate bug reports in our first two weeks of production.

Have a project on this topic?

Leave a brief summary — I’ll get back to you within 24 hours.

Get in touch