What happens after BGContinuedProcessingTask "expires"?

If I create a BGContinuedProcessingTaskRequest, register it, and then "do work" within it appropriately reporting progress, and before my task has finished doing all the work it had to do, its expirationHandler triggers...

does the task later try again?

Or does it lose the execution opportunity until the app is next re-launched to the foreground?

In my testing, I never saw my task execute again once expired (which suggests the latter?).

I was able to easily force this expiry by starting my task, backgrounding my app, then launching the iOS Camera App. My example is just using test code inspired from https://developer.apple.com/documentation/backgroundtasks/performing-long-running-tasks-on-ios-and-ipados

                let request = BGContinuedProcessingTaskRequest(identifier: taskIdentifier, title: "Video Upload", subtitle: "Starting Upload")
                request.strategy = .queue
                
                BGTaskScheduler.shared.register(forTaskWithIdentifier: taskIdentifier, using: nil) { task in
                    guard let task = task as? BGContinuedProcessingTask else { return }
                    print("i am a good task")
                    var wasExpired = false
                    
                    task.expirationHandler = {
                        wasExpired = true
                    }

                    let progress = task.progress
                    progress.totalUnitCount = 100
                    while !progress.isFinished && !wasExpired {
                        progress.completedUnitCount += 1
                        let formattedProgress = String(format: "%.2f", progress.fractionCompleted * 100)
                        task.updateTitle(task.title, subtitle: "Completed \(formattedProgress)%")
                        sleep(1)
                    }
                    
                    if progress.isFinished {
                        print ("i was a good task")
                        task.setTaskCompleted(success: true)
                    } else {
                        print("i was not a good task")
                        task.setTaskCompleted(success: false)
                    }
                }
                
                try? BGTaskScheduler.shared.submit(request)

Apologies if this is clearly stated somewhere and I'm missing it.

If I create a BGContinuedProcessingTaskRequest, register it, and then "do work" within it appropriately reporting progress, and before my task has finished doing all the work it had to do, its expirationHandler triggers...

As a small side comment, you don't actually need to "do" any of the work inside your launchHandler. If you choose, you can just capture the task object (for progress, completion, etc.) and immediately return. See this forum thread for a longer discussion of these issues.

does the task later try again?

No. The idea is that the API is specifically extending foreground time, so it wouldn't really make sense to "resume" it later.

Or does it lose the execution opportunity until the app is next re-launched to the foreground?

In my testing, I never saw my task execute again once expired (which suggests the latter?).

Yes, both of points those are correct.

I was able to easily force this expiry by starting my task, backgrounding my app, then launching the iOS Camera App.

That's actually a little surprising to me. What device are you using? If you haven't already, please file a bug on this and post the bug number once it's filed.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Hi Kevin,

we’re investigating a reproducible Camera-related expiration and wondering whether it connects to the progress-reporting issues discussed here.

On an iPhone14,3 running iOS 26.6.1 (23G83), we start a BGContinuedProcessingTask in the foreground, background our app, and open Apple Camera in Photo preview without taking a picture.

Across six sequential trials on the same phone:

  • The continued task expired in all six; our app process remained alive.
  • Five captures explicitly identified Viewfinder Policy for the affected task.
  • Settings, Calculator, Safari and video controls preserved the continued task.
  • We directly advance task.progress.completedUnitCount approximately every five seconds. The largest locally recorded update gaps were about 5.5–5.6 seconds, so we aren’t relying solely on fractional child progress.

The phone was externally powered with Low Power Mode off. UI automation and bounded system logging were active during the Camera tests.

One implementation detail that may matter: our use case is ongoing community hosting. Progress currently tracks connected seconds, and we extend totalUnitCount as the session continues. We have not yet reproduced this with a standalone, finite-work sample, and we don’t want to assume that this ongoing progress model is supported.

Is Camera’s Viewfinder Policy expected to expire a continued task despite regular progress updates? Could our progress model affect that decision, and what reproduction or diagnostics would best distinguish those possibilities?

Is Camera’s Viewfinder Policy expected to expire a continued task despite regular progress updates?

Yes, this is expected behavior. The camera generates significant system load (much higher than you might expect) and is important enough that Duet shuts down other activity (including the continued processing task) to ensure it can't be disrupted.

Could our progress model affect that decision, and what reproduction or diagnostics would best distinguish those possibilities?

There isn't anything you can do to prevent it, however, if you schedule a task as ".queue" when your task expires, that task may[1] start up when the camera session ends.

[1] I haven't specifically tested this, but am basing that on a review of the code involved.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Hi Kevin, thanks for the quick response/suggestion!

We tried submitting a replacement task with .queue when Camera expired the running task, but encountered a foreground-check error.

Device: iPhone14,3, iOS 26.6.1 (23G83), externally powered.

Our sequence was:

  1. While our app was foregrounded, register a unique replacement task identifier and create its request with strategy = .queue.
  2. Start the original task, background our app, then open Camera.
  3. In the original task’s expiration handler, submit the prepared replacement before calling setTaskCompleted on the original task.
  4. Close Camera normally after 90 seconds and observe for 15 minutes without reopening our app.

In two valid trials, BGTaskScheduler.shared.submit(...) returned without throwing, but the replacement’s launch handler never ran. Our app process remained alive. Ordinary processing/refresh tasks ran separately.

In the second trial, dasd logged this error for the exact replacement identifier at 2026-10-29 16:19:50.896323 UTC:

bgContinuedProcessing-io.korom.krm.extended-hosting.A6008727-F8B5-423C-8C30-5E68965F03AD: Foregrounded apps (<private>) don't include expected identifier: <private>

This looks related to the missing-error behavior discussed in thread 807370. We therefore aren’t treating the successful API return as proof that iOS actually admitted the replacement to its queue.

Is queued resubmission from an expiration handler supported while the app is backgrounded on this iOS version? If so, what request/submission sequence avoids this foreground check?

These tests used our ongoing-hosting app; we still haven’t established a standalone finite-work reproduction.

This looks related to the missing-error behavior discussed in thread 807370. We therefore aren’t treating the successful API return as proof that iOS actually admitted the replacement to its queue.

This issue is actually fixed by the deprecation of submitTaskRequest() and by its replacement submitTaskRequest()

Is queued resubmission from an expiration handler supported while the
app is backgrounded on this iOS version?

No, that was actually an oversight on my part. I was in a lab yesterday, so I'm afraid you got my less than fully thought-out answer. There may be some cases[1] where it does work, but it's probably not reliable enough to be useful.

[1] More specifically, historically, there have been cases where we "resigned" the foreground app instead of "backgrounding" it— for example, this is what the old full-screen phone UI did. If those triggered expiration, then queuing would work. However, we've generally been moving away from those "resign" cases, and I don't know which (if any) are actually still around.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware