BGContinuedProcessingTask expiring unpredictably

I've adopted the new BGContinuedProcessingTask in iOS 26, and it has mostly been working well in internal testing. However, in production I'm getting reports of the tasks failing when the app is put into the background.

A bit of info on what I'm doing: I need to download a large amount of data (around 250 files) and process these files as they come down. The size of the files can vary: for some tasks each file might be around 10MB. For other tasks, the files might be 40MB. The processing is relatively lightweight, but the volume of data means the task can potentially take over an hour on slower internet connections (up to 10GB of data).

I set the totalUnitCount based on the number of files to be downloaded, and I increment completedUnitCount each time a file is completed.

After some experimentation, I've found that smaller tasks (e.g. 3GB, 10MB per file) seem to be okay, but larger tasks (e.g. 10GB, 40MB per file) seem to fail, usually just a few seconds after the task is backgrounded (and without even opening any other apps). I think I've even observed a case where the task expired while the app was foregrounded!

I'm trying to understand what the rules are with BGContinuedProcessingTask and I can see at least four possibilities that might be relevant:

  1. Is it necessary to provide progress updates at some minimum rate? For my larger tasks, where each file is ~40MB, there might be 20 or 30 seconds between progress updates. Does this make it more likely that the task will be expired?
  2. For larger tasks, the total time to complete can be 60–90 mins on slower internet connections. Is there some maximum amount of time the task can run for? Does the system attempt some kind of estimate of the overall time to complete and expire the task on that basis?
  3. The processing on each file is relatively lightweight, so most of the time the async stream is awaiting the next file to come down. Does the OS monitor the intensity of workload and suspend the task if it appears to be idle?
  4. I've noticed that the task UI sometimes displays a message, something along the lines of "Do you want to continue this task?" with a "Continue" and "Stop" option. What happens if the user simply ignores or doesn't see this message? Even if I tap "Continue" the task still seems to fail sometimes.

I've read the docs and watched the WWDC video, but there's not a whole lot of information on the specific issues I mention above. It would be great to get some clarity on this, and I'd also appreciate any advice on alternative ways I could approach my specific use case.

Based on the response from the engineer over on this post, I looked at the Console logs for the dasd process and found this:

Task has not reported progress within expected cadence, marking stalled (time without update: -36.27811694145203)

Does -36 mean 36 seconds since the last update? That seems plausible given my situation. What cadence do I need to hit to satisfy the system?

Not sure what to do now. This limitation seems to make BGContinuedProcessingTask unsuitable for my use case, since the size of the downloaded files can vary, as can the speed of the user's connection.

I could try to feed in fake progress updates to keep the task alive. Or I could try to monitor the byte count of downloading files and use that as the basis for progress updates (however, my previous experience trying to pass the byte count from URLSessionDelegate through an async stream was a nightmare).

In any case, it would still be useful if someone from engineering could respond to my above queries, so that I can better understand if there might be other issues to contend with (not just the progress cadence).

First off, sorry for the delay on this. I'm afraid your thread got overlooked until today...

Does -36 mean 36 seconds since the last update? That seems plausible given my situation.

Yes. FYI, it’s a negative number because it’s using NSDate.timeIntervalSinceNow, which ends up returning a negative number.

What cadence do I need to hit to satisfy the system?

We haven't published an actual number, but I believe it's ~30s. However, that's actually a pretty poor experience overall, as your progress bar is going to end up stalling for an extended period of time. Informally, I've generally found that a progress bar needs to update every ~0.1s (10x/sec) to ~1-2s in order to look "right".

This limitation seems to make BGContinuedProcessingTask unsuitable for my use case, since the size of the downloaded files can vary, as can the speed of the user's connection.

Keep in mind that the other alternative is that the user cancels your task entirely, since it looks like nothing is happening. From that perspective, 30s is a very long time for a progress bar to stall.

I could try to feed in fake progress updates to keep the task alive. Or I could try to monitor the byte count of downloading files and use that as the basis for progress updates (however, my previous experience trying to pass the byte count from URLSessionDelegate through an async stream was a nightmare).

You can use the progress delegate that provides "raw" rate data; however, the easier option is to use the URLSessionTask.Progress object. With single downloads, you can use that Progress object directly, while more complicated scenarios can use child progress and the flow described here.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Thanks for the advice!

Informally, I've generally found that a progress bar needs to update every ~0.1s (10x/sec) to ~1-2s in order to look "right".

I guess this depends on the magnitude of the task. If the task takes an hour, then updating every 1s would mean that each update moves the progress bar by a fraction of a pixel. My current setup moves the progress bar about 4px at a time, which looks pretty smooth to me.

You can use the progress delegate that provides "raw" rate data; however, the easier option is to use the URLSessionTask.Progress object. With single downloads, you can use that Progress object directly, while more complicated scenarios can use child progress and the flow described here.

Thanks for the pointers. I looked into these options but, in the end, it feels like trying to tie the progress to bytes downloaded (rather than number of files completed) is going to open up other issues, notably:

  1. If a file fails to download for some reason (and has to be restarted), I will need to subtract out the bytes downloaded from the progress or, in some sense, track each file's progress. Not impossible, but feels fraught with state management bug potential.

  2. My task involves downloading and processing files. In some cases, the processing is very small relative to the download time, but in other cases, the processing can take longer than the download, so linking progress only to the download part is not ideal. That's why I originally chose to link progress to file completion.

  3. The modifications I'd need to make to my code (passing download progress from the URLSession delegate through an AsyncStream back to the calling task) will result in more complexity that will be hard to test, and background URLSessions are already really hard to work with.

Overall, my sense is that BGContinuedProcessingTask is better suited to tasks that are primarily CPU-bound where the rate of progress and time-to-completion is fairly predictable a-priori. For tasks that are more network-bound and where progress is heavily dependent on the volume of user data, it becomes hard to guarantee a particular rate of progress.

Since BGContinuedProcessingTask seems to be quite trigger happy and doesn't distinguish between user cancellation and system cancellation, I don't think it's going to be the right option for me. Which is a shame because I was quite excited about it when I saw it at WWDC.

Anyway, thanks for the advice, Kevin, and for being very responsive (also to other threads on this forum). It's much appreciated!

I guess this depends on the magnitude of the task. If the task takes an hour, then updating every 1s would mean that each update moves the progress bar by a fraction of a pixel. My current setup moves the progress bar about 4px at a time, which looks pretty smooth to me.

OK. Again, progress reporting here is also about telling the system you're still active, not just updating the interface. However, the other option here is to use multiple tasks so that progress is occurring across more progress bars.

  1. If a file fails to download for some reason (and has to be restarted), I will need to subtract out the bytes downloaded from the progress or, in some sense, track each file's progress. Not impossible, but feels fraught with state management bug potential.

Yes, but using NSURLSessionTask.progress should largely isolate you from these issues.

  1. My task involves downloading and processing files. In some cases, the processing is very small relative to the download time, but in other cases, the processing can take longer than the download, so linking progress only to the download part is not ideal.

This is part of why the child progress system exists, as it lets you break down work into separate progress objects which can then report work independently.

Since BGContinuedProcessingTask seems to be quite trigger-happy and doesn't distinguish between user cancellation and system cancellation,

As a quick note on user/system cancellation, this is something the team is aware of and plans to address.

Anyway, thanks for the advice, Kevin, and for being very responsive (also to other threads on this forum). It's much appreciated!

You're very welcome.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

BGContinuedProcessingTask expiring unpredictably
 
 
Q