---
title: 'Background Processing Limits'
source: 'https://academia.sh/en/courses/cross-platform-production/background-processing-limits'
course: 'Cross-Platform Production Practice'
language: en
updated: '2026-10-21T05:02:50+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Explains the background task mechanisms of the two platforms and the limits the operating system imposes'
    - 'Distinguishes low power mode as a condition, not an operability level'
    - 'Measures that media backup is deferred when background capability is restricted'
    - 'Shows that deferred work does not run but is not lost, while disabled work is not left for later'
---

# Background Processing Limits

Background processing limits describe the rules the operating system sets for running work while the app is not visible. The lesson measures that media backup is deferred and live location is disabled when background capability is restricted.

In the previous lesson, changes made offline entered a queue and were to be sent when the network returned. Who starts the sending was not asked. The user may have closed the app, or the phone may be in a pocket. For an app to run work while it is not visible, the operating system has to allow it, and the operating system does not give that permission generously. This lesson explains the background processing mechanisms of the two platforms and the limits they bring, then measures how much those limits affect a feature.

The course question stays the same: how well does this feature work under this condition? The nine records in the opening lesson came out as five **full**, one **degraded**, two **deferred**, and one **disabled**. This lesson does not change that count; it measures only its own slice.

## The Operating System Decides

Working in the background is the operating system's decision, not the app's. The app asks for a job; the operating system looks at conditions such as battery, network, the user's habits, and the load from other apps, and decides when the job runs, or even whether it runs at all. So a background job never means "run now"; it means "run at a suitable moment".

On iOS the app registers a task request, and when the system finds it suitable, it wakes the app for a short time. The app does not set the wake time. On Android the job is handed to a work manager together with the conditions it needs; the manager runs the job when the conditions are met and the device's power management allows it. With both mechanisms the app has no exact clock. The limits on duration and frequency are set by the system version and the user's settings; this lesson does not write these limits as numbers, because they change with every version and every device.

The iOS snippet below shows requesting a backup task. The snippet is not measured.

```swift
// Platform code: iOS background task
func scheduleBackup() {
    let request = BGProcessingTaskRequest(identifier: "app.media-backup")
    request.requiresNetworkConnectivity = true
    request.requiresExternalPower = false
    try? BGTaskScheduler.shared.submit(request)
}
```

On Android the same request is handed to the work manager together with its conditions. The snippet is not measured.

```kotlin
// Platform code: Android background task
fun scheduleBackup(context: Context) {
    val constraints = Constraints.Builder()
        .setRequiredNetworkType(NetworkType.CONNECTED)
        .build()
    val request = OneTimeWorkRequestBuilder<BackupWorker>()
        .setConstraints(constraints)
        .build()
    WorkManager.getInstance(context).enqueue(request)
}
```

The two snippets ask for the same thing: a backup job that runs while there is a network. Both create a registration; the job does not run right away. Whether it runs is up to the system's answer.

## Low Power Mode

The user or the system can turn on **low power mode** to save battery. While the mode is on, the system cuts or thins out background work: refreshing stops and scheduled jobs run later. While the app is open, the network and storage are still there; what disappears is the permission to run in the background.

This lesson treats low power mode as a **condition**. A condition is the set of capabilities present at that moment: in low power mode, `network` and `storage` are present and `background` is not. Under the same condition one feature can come out full, another deferred, and a third disabled.

## Building the Features

Measuring needs the course core. The core is familiar from the first lesson and is given again in the same form in every lesson; this lesson does not touch it.

```javascript
// operability.mjs — classifies how fully a feature works under a condition
export const LEVELS = ["full", "degraded", "deferred", "disabled"];
const FALLBACKS = ["degraded", "deferred"];

export function feature(name, needs, fallback = {}) {
  return { name, needs, fallback };
}

export function condition(name, has) {
  return { name, has };
}

function judge(item, cond) {
  const missing = item.needs.filter((need) => !cond.has.includes(need)).sort();
  let level = "full";
  for (const need of missing) {
    const given = FALLBACKS.includes(item.fallback[need]) ? item.fallback[need] : "disabled";
    if (LEVELS.indexOf(given) > LEVELS.indexOf(level)) level = given;
  }
  return { level, feature: item.name, condition: cond.name, missing };
}

export function assess(features, conditions) {
  const records = [];
  for (const cond of conditions) {
    for (const item of features) records.push(judge(item, cond));
  }
  return records;
}

export function printLevels(records) {
  for (const r of records) {
    const missing = r.missing.length === 0 ? "-" : r.missing.join(",");
    console.log(`${r.level}: ${r.feature} @ ${r.condition} missing=${missing}`);
  }
}

export function countLevels(records) {
  for (const name of LEVELS) {
    console.log(`${name}: ${records.filter((r) => r.level === name).length}`);
  }
  console.log(`affected: ${records.filter((r) => r.level !== "full").length}`);
}
```

The lesson's helper file builds three features. `MediaBackup` backs up photos; it needs `network` and `background`, and when background is missing it postpones its work, so its fallback is `deferred`. `LiveLocation` shares location continuously; it needs the same two capabilities and has no fallback, because sharing a stale location is meaningless. `Catalog` is a product list and needs only `network`. `stateOf(link)` builds a condition from the connection and background state.

```javascript
// background.mjs — builds the background features and conditions
import { feature, condition } from "./operability.mjs";

export const mediaBackup = feature("MediaBackup", ["network", "background"], { background: "deferred" });
export const liveLocation = feature("LiveLocation", ["network", "background"]);
export const catalog = feature("Catalog", ["network"]);

export function stateOf(link) {
  const has = [];
  if (link.network) has.push("network");
  if (link.background) has.push("background");
  return condition(link.name, has);
}
```

The model of the scheduler is in a second helper file. `schedule` looks at a record's operability: if it is full, the job runs; if it is deferred, the job is added to the waiting list; if it is disabled, the job does not run and does not enter the waiting list. `runWindow` imitates a window given by the system: the window runs a certain number of jobs from the waiting list.

```javascript
// scheduler.mjs — builds background work according to the operating system's window
export function schedule(record, pending, job) {
  if (record.level === "deferred") {
    pending.push({ feature: record.feature, job });
    return "waiting";
  }
  return record.level === "full" ? "running" : "skipped";
}

export function runWindow(pending, slots) {
  return pending.splice(0, slots).length;
}
```

The first measurement runs backup and the catalog in two states: the app in the foreground and background restricted.

```javascript
// main.mjs
import { assess, printLevels } from "./operability.mjs";
import { mediaBackup, catalog, stateOf } from "./background.mjs";
import { schedule } from "./scheduler.mjs";

const states = [
  { name: "foreground", network: true, background: true },
  { name: "background-restricted", network: true, background: false },
];
const records = assess([mediaBackup, catalog], states.map(stateOf));
printLevels(records);
const pending = [];
for (const r of records.filter((x) => x.feature === "MediaBackup")) {
  console.log(`${r.condition}: ${schedule(r, pending, "photo-1")}`);
}
console.log(`waiting: ${pending.length}`);
```

```text
full: MediaBackup @ foreground missing=-
full: Catalog @ foreground missing=-
deferred: MediaBackup @ background-restricted missing=background
full: Catalog @ background-restricted missing=-
foreground: running
background-restricted: waiting
waiting: 1
```

While the app is in the foreground, backup is **full** and the job runs right away. While background is restricted, backup is **deferred** and the job enters the waiting list. `Catalog` is **full** in both states, because it depends only on the network.

**OS3.** The model builds background as a single capability: either it is there or it is not. A real system gives background work short and unpredictable windows; when the job runs, how many windows it takes to finish, and whether it ever runs are the system's decision. In the model, `runWindow` takes the size of the window as a job count chosen for the lesson; that number says nothing about the behavior of a system. The model builds this operability's rule; the model is not the framework itself.

## Deferred Work Is Not Lost

Deferred work has not run, but it has not been lost. It stays in the waiting list and runs when the system gives a window. The second measurement puts three backup jobs in the queue under the restricted condition and gives two windows. Under the same condition `LiveLocation` asks for three location updates.

```javascript
// main.mjs
import { assess, printLevels } from "./operability.mjs";
import { mediaBackup, liveLocation, stateOf } from "./background.mjs";
import { schedule, runWindow } from "./scheduler.mjs";

const states = [
  { name: "background-restricted", network: true, background: false },
  { name: "foreground", network: true, background: true },
];
const records = assess([mediaBackup, liveLocation], states.map(stateOf));
const restricted = records.filter((r) => r.condition === "background-restricted");
printLevels(restricted);
const pending = [];
for (const job of ["photo-1", "photo-2", "photo-3"]) schedule(restricted[0], pending, job);
for (const job of ["location-1", "location-2", "location-3"]) schedule(restricted[1], pending, job);
console.log(`first window: ${runWindow(pending, 2)} ran, ${pending.length} waiting`);
console.log(`second window: ${runWindow(pending, 2)} ran, ${pending.length} waiting`);
```

```text
deferred: MediaBackup @ background-restricted missing=background
disabled: LiveLocation @ background-restricted missing=background
first window: 2 ran, 1 waiting
second window: 1 ran, 0 waiting
```

The three backup jobs finished in two windows: in the first, two ran and one waited; in the second, the remaining one ran. The location updates did not appear in any window, because `schedule` does not add the job of a disabled record to the waiting list. A disabled feature does not run either, but its work is not left for later; the user cannot share their location in the background, and that sharing is not done at the next opportunity.

## Low Power Mode Is Not a Level

The third measurement puts low power mode side by side with the app-in-foreground condition. Three features run under two conditions, and only the low power records are printed.

```javascript
// main.mjs
import { assess, printLevels } from "./operability.mjs";
import { catalog, mediaBackup, liveLocation, stateOf } from "./background.mjs";

const states = [
  { name: "foreground", network: true, background: true },
  { name: "low-power", network: true, background: false },
];
const records = assess([catalog, mediaBackup, liveLocation], states.map(stateOf));
printLevels(records.filter((r) => r.condition === "low-power"));
```

```text
full: Catalog @ low-power missing=-
deferred: MediaBackup @ low-power missing=background
disabled: LiveLocation @ low-power missing=background
```

With low power mode on, three features fell to three different levels. Low power mode is a condition; degraded is an operability level, and the two words are not the same thing. The mode says which capability decreased; the level is decided by the fallback the feature defined for that missing capability.

## The Bounding Measurement

This lesson's focus record is `MediaBackup @ background-restricted`. The last measurement runs three features under three conditions and prints the records of the background-restricted condition, the count, and the waiting jobs.

```javascript
// main.mjs
import { assess, printLevels, countLevels } from "./operability.mjs";
import { mediaBackup, liveLocation, catalog, stateOf } from "./background.mjs";
import { schedule } from "./scheduler.mjs";

const states = [
  { name: "foreground", network: true, background: true },
  { name: "background-restricted", network: true, background: false },
  { name: "low-power", network: true, background: false },
];
const records = assess([mediaBackup, liveLocation, catalog], states.map(stateOf));
const restricted = records.filter((r) => r.condition === "background-restricted");
printLevels(restricted);
countLevels(records);
const pending = [];
for (const r of restricted) {
  for (const job of ["job-1", "job-2", "job-3"]) schedule(r, pending, job);
}
const waiting = (name) => pending.filter((p) => p.feature === name).length;
console.log(`waiting: MediaBackup ${waiting("MediaBackup")}, LiveLocation ${waiting("LiveLocation")}`);
```

```text
deferred: MediaBackup @ background-restricted missing=background
disabled: LiveLocation @ background-restricted missing=background
full: Catalog @ background-restricted missing=-
full: 5
degraded: 0
deferred: 2
disabled: 2
affected: 4
waiting: MediaBackup 3, LiveLocation 0
```

The first line carries the lesson's operability: when background capability is restricted, backup came out **deferred**. Of the nine records, five are **full**, two are **deferred**, and two are **disabled**; four are affected. The two deferred records are two conditions of the same feature: restricted and low power.

The last line shows how this differs from the question of M12/K04. That lesson asked whether a piece ran in this request. Here neither backup nor location sharing ran in the background, but their situations are different. Deferred backup has not run and has not been lost: three jobs wait. Disabled location sharing has not run either, but it has no waiting job. Operability does not classify whether a job ran; it classifies what happens to the job when it did not run.

## Summary

- This lesson's operability: **deferred**. `MediaBackup @ background-restricted` came out deferred because `background` is missing and a `deferred` fallback is defined; `LiveLocation` stayed **disabled** under the same condition because it has no fallback.
- Working in the background is the operating system's decision. On iOS the system wakes the app when it finds the task request suitable, and on Android the work manager runs the job when the conditions are met; the limits on duration and frequency change with the version and the settings.
- Low power mode is a condition, not an operability level: under the same mode `Catalog` came out **full**, `MediaBackup` **deferred**, and `LiveLocation` **disabled**.
- Deferred work has not run but has not been lost: three backup jobs finished in two windows. The work of a disabled feature does not enter the waiting list.
- Of the nine records, five are **full**, two are **deferred**, and two are **disabled**; four are affected.

## Next Step

In this lesson, background capability was either there or not; the connection was always there. In reality the connection also drops, slows down, and comes back. The next lesson, `Connectivity State Management`, covers monitoring the connection, a weak connection, retry with exponential backoff, and showing the connection state to the user. It then measures how well an image gallery works on a weak connection.
