Skip to content

23. Background work and notifications

The question. What does this app do when it is not on screen, what wakes it up, and who writes the notifications you see?

A mobile app does not decide when it runs. The operating system does. An app that leaves the screen is suspended within seconds, and its process can be removed at any time after that. Everything the app does while you are not looking at it happens inside a window that the operating system opened for a stated reason and will close on its own schedule.

Four of the seven constraints (Chapter 2) create this concern.

Platform rules. The operating system grants background execution in a few fixed kinds, each with its own trigger and time limit. Notifications are a platform service with its own permission, its own settings screen and its own display rules.

Limited resources. Every background wake costs battery, and the operating system keeps a budget per app. The budget shrinks for an app the user rarely opens and shrinks further in low-power mode.

Process death. Background workers cannot assume memory survives between two runs. Each run starts from the local store and must leave it consistent if the run is cut short.

Human attention. A notification interrupts. Platforms give the user controls to mute an app, and an app that wastes interruptions loses the channel entirely.

The practical result is a split. Work the user is waiting for runs under a visible indicator. Everything else runs when the operating system chooses, or does not run. A teardown of this concern finds out which kind of grant each feature depends on, and what happens to the feature when the grant is withheld.

Chapter 14 covers how fast an event reaches the device and by which path. This chapter starts where that one stops: what the device does with the event, and what the app does with the time it is given.

The operating system grants background execution in four kinds. They are ordered here from the shortest to the longest.

An exit grace period on leaving. When the app leaves the screen, it can ask for a short extension to finish what it is doing. The extension lasts from a few seconds to about half a minute. This chapter calls it the exit grace period. Teams use it to send the last entries in the outbox, save a draft and close connections cleanly. It costs nothing to request and it cannot be relied on for anything long. A mutation that needs three retries on a weak network does not fit in it.

A deferred task. The app registers a piece of work with conditions, such as “when a network is present” or “when charging and idle”. The operating system runs it at a time it chooses. It may run within minutes, or tonight, or never. This chapter calls it a deferred task. Teams use it for work nobody is waiting on: refreshing the first screen’s data, retrying the outbox, cleaning caches, uploading logs. Its cost is unpredictability. No feature with a deadline can depend on it.

User-visible long-running work. The app declares that it is doing something the user knows about, from a short list the platform accepts: playing audio, giving directions, holding a call, recording, tracking a workout, finishing a transfer the user started. The operating system lets the process keep running for as long as the activity lasts. In exchange, the platform shows the user that it is happening, through a persistent notification, an indicator in the status area or controls on the lock screen. This chapter calls it user-visible work. The cost is that the indicator is mandatory, and that the store reviews whether the declared activity is real.

A push wake. A platform push arrives and the operating system gives the app a few tens of seconds to react. The push may be a silent push, or a visible notification that also requests a wake. Teams use it to pull new data into the local store, to build or withdraw a notification, and to refresh other surfaces. Its cost is that the operating system rations it, and withholds it entirely in several states described in Section 23.4.

Two further grants exist and belong to other chapters. A transfer can be handed to the operating system, which carries it out with no app process at all (Chapter 18). Hardware can wake the app on an event such as entering a region or an accessory coming into range (Chapter 24).

KindWho starts itHow long it runsWhen it runsWhat the user sees
Exit grace periodThe app, on leaving the screenSeconds to about half a minuteAt onceNothing
Deferred taskThe operating systemUp to a few minutesWhen conditions and budget suitNothing
User-visible workThe user, through the appAs long as the activity lastsAt onceA mandatory indicator or controls
Push wakeThe backend, through platform pushA few tens of secondsOn arrival, if the system allowsNothing, or a notification

A design that has none of these is also valid. Such an app does nothing in the background. It is fully up to date only after you open it and wait. Many apps are built this way on purpose, because every other option adds states to test.

Figure 23.1 shows the notification pipeline, from an event on the backend to the lock screen. It also shows the two places a notification can be written.

Figure 23.1. The notification pipeline, with both places a notification can be written.

The pipeline has five stages, and each one makes a decision you can observe.

  1. Services record an event. A message is stored, an order changes state, a price crosses a threshold.
  2. The notification service decides whether anyone should be told. It reads the recipient’s preferences, mutes, quiet hours and rate limits. It decides which of the user’s devices to address. It writes the text, or decides that the device will write it.
  3. Async delivery hands the message to the platform push service, addressed to a device token, with an expiry and optionally a replacement key. Section 23.7 explains both.
  4. The platform push service delivers it, or stores it while the device is unreachable.
  5. Platform services on the device act on it. They display it as sent, or wake background workers, or both.

Stage 5 holds the main design choice of this chapter.

A system-displayed notification. The payload contains the title, the text and the sound. The operating system draws it without running any app code. It appears whatever state the app is in, including after a force-quit. The app’s local store is unchanged, so opening the app starts a fetch. The backend must know the full text, in the user’s language, at the moment it sends.

An app-built notification. The payload contains an identifier, or encrypted content, and a request to wake. Background workers, or a small short-lived process that the platform runs for this one purpose, receive it. They pull or decrypt the item, write it to the local store, compose the notification from local data and display it. The notification can use things the backend does not have: the name you gave a contact on this device, content only the device can decrypt, the current state of the local store. Its cost is dependence on the push wake.

Most mature apps combine the two. The payload carries fallback text that the operating system shows if no app code runs, and the app replaces it when it does run. From outside, this looks like a notification whose level of detail changes with the state of the app.

System-displayedApp-builtCombined
Who writes the textThe backendThe clientThe client, with backend fallback
After a force-quitAppears as usualMay not appearAppears with less detail
Local store when you open the appUnchangedAlready holds the itemDepends on whether the wake ran
Can show device-only dataNoYesYes, when the wake ran
Depends on the wake budgetNoYesOnly for the richer form

One case removes the ambiguity. In an app with end-to-end encryption, the backend cannot read the content. If a notification from that app shows the message text, code on the device decrypted it. That notification is app-built, and the claim is Inferred from the product’s own stated design.

A push wake has a deadline. The handler must put the visible result first and leave the local store consistent if time runs out.

function onPushWake(push, deadline):
match push.kind:
Withdraw:
removeNotification(push.itemId) // read on another device
NewItem:
item = pullOrDecrypt(push.itemId)
if item == null or deadline.isNear():
show(push.fallbackText) // never end the wake with nothing shown
else:
transaction(localStore):
upsert(item)
show(buildNotification(item), replaceKey = item.threadId)
setBadge(countUnread(localStore)) // derived state, computed on the device
finish() // overrunning the deadline costs future budget

The last comment matters for a teardown. The operating system tracks how each app uses its wakes. An app that overruns, crashes or does nothing useful in them is granted fewer. Background behavior that gets worse over weeks on one device, and is fine on a fresh install, is consistent with a spent budget.

Every grant in Section 23.2 can be withheld. The conditions differ between platforms and between device makers, and they change with each operating system release. The categories are stable.

Quotas. Deferred tasks and push wakes are counted per app. The operating system learns when you usually open the app and prefers to run its tasks shortly before. An app opened every morning is refreshed before the morning. An app opened once a month is refreshed rarely or never.

Low-power mode. The user asks the device to save battery. Deferred tasks are postponed, silent pushes are delayed or dropped, and fetches in the background are cut. User-visible work and system-displayed notifications continue.

A force-quit app. On some platforms, a force-quit tells the operating system that the user does not want the app to run. Push wakes and deferred tasks stop until the user opens the app again. On other platforms a force-quit has no such meaning, and the device maker’s own battery manager applies a similar rule to apps it judges inactive. In both cases a system-displayed notification still appears.

An app the user rarely opens. Beyond a shrinking quota, some platforms put an unused app into a dormant state after weeks. Its scheduled work is cancelled, its notifications may be silenced or moved to a digest, and its permissions may be withdrawn.

A user setting. Most platforms let the user turn off background refresh per app, restrict background data, or mark the app as restricted. Data-saver mode has a similar effect on background fetches.

A restarted device. Until the user unlocks the device for the first time after a restart, secure hardware-backed storage is closed. A push wake in that state cannot read the session, so it cannot pull. Only system-displayed notifications work.

Figure 23.2 puts these together for one incoming push.

Figure 23.2. What one platform push can lead to, depending on its payload and the state of the app.

The figure explains why the limits are useful to a teardown. Each limit removes the push wake and leaves the system-displayed path intact. Applying a limit and watching what survives tells you which path a feature uses. A notification that survives every limit is system-displayed. One that disappears or loses detail is app-built.

Deferred tasks have no such clean test. You cannot make one run. You can only leave the app alone for a long time and check afterwards whether its local store moved forward, which is probe P23.9.

User-visible work is the one grant that lasts, and it is the easiest to observe because the platform insists on showing it.

The indicator takes several forms: a persistent notification that cannot be swiped away, a colored marker in the status area, playback controls on the lock screen, a banner that returns you to the app. The form tells you the declared activity. Playback controls mean audio. A marker with a timer means a call or a recording. A location marker means tracking.

Three design points follow.

The activity must be the real one. An app that plays silence to stay alive, or declares navigation in order to poll, breaks store rules. You will rarely see it in a reviewed app, and when you do, the mismatch between the indicator and the feature is the evidence.

The work must survive process death anyway. The grant lowers the chance that the process is removed. It does not remove it. A well-built player or tracker saves its position to the local store as it goes, and resumes from there. Chapter 17 covers playback and Chapter 24 covers tracking.

A user-started transfer is a borderline case. Some platforms let an upload or export continue under a progress indicator. Others require it to be handed to the operating system, which runs it with no indicator from the app. A transfer that completes while the app is in the background with nothing visible is therefore not proof of a rule being bent. It is Speculative evidence of a transfer handed to the system.

A notification is a small screen with rules. How an app uses those rules exposes the payload, the wake and the backend behind it.

Notifications that stack by conversation, order or topic carry a group identifier. The identifier comes from the backend’s data model, so the grouping shows what the backend treats as one thread. A summary line such as a count of further items is written by the operating system or by app code. A summary that names local data, such as contact names you assigned, is app-built.

Buttons under a notification, or a text field for a reply, are declared by the app in advance as a set per notification type. Tapping one starts the app in the background for a few seconds to carry it out. This is a push wake in reverse: the trigger is the user and the work is a mutation.

A well-built action writes the mutation to the outbox (Chapter 12) and sends it within the window. An action that opens the app instead shows that the team did not build the background path, or that the action needs a check the background cannot perform, such as a biometric prompt.

An image in a notification is not in the payload. The payload is a few thousand bytes at most. The payload carries an address, and a short-lived process on the device downloads the image before the notification is displayed. This tells you app code ran on arrival. On a slow network, the same notification arrives without its image, because the download did not finish within the time allowed.

A notification can replace an earlier one with the same identifier. A score that changes, a delivery that advances a stage and an edited message all use this. The backend sends a new push with the same replacement key, or the app rebuilds the notification during a wake. Either way the count of notifications does not grow.

A notification can be removed without the user touching it. The common case is reading the item on another device. The backend sends a push that names the item, and app code removes the notification. On most platforms this needs a push wake, so it is subject to every limit in Section 23.4. A withdrawal that works while the app is in the background and fails after a force-quit is the signature of that dependence.

The number on the app’s icon has two possible sources.

  • The backend computes it and puts it in the payload. The operating system sets it with no app code. It is correct across devices and changes even after a force-quit. It requires the backend to keep an unread count per user.
  • The client computes it as derived state from the local store during a wake or on leaving the app. It can include things only the device knows. It goes stale when wakes are refused.

A badge that disagrees with the count shown inside the app points to two sources that have drifted apart.

Platforms let an app declare named categories of notification, each with its own switch, sound and importance in the system settings. The list is readable by any user and is a map of the product’s event types as the team sees them.

Preferences kept only inside the app’s own settings screen are a different design. They are stored on the backend and applied by the notification service before anything is sent. The two can be told apart: a category switched off in system settings still costs a push that the device discards, and one switched off in the app is never sent. From outside, the visible difference is that in-app preferences follow you to a second device and system settings do not.

Some notifications break through a focus or do-not-disturb setting, and some arrive silently in a list. These levels are set per notification by the sender, within what the platform and the user allow. A level that seems wrong for the content shows a default nobody tuned. Notifications that arrive at the same local time every day, in every time zone you travel to, are scheduled on the backend per user.

Tapping a notification is a deep link. Where you land, and whether back leads somewhere sensible, is covered in Chapter 8.

23.7 Local scheduling, and notifications sent to an offline device

Section titled “23.7 Local scheduling, and notifications sent to an offline device”

An app can ask the operating system to show a notification at a future time or on a condition, such as arriving at a place. No push and no network are involved. The operating system holds the request and shows it even if the app has no process. This chapter calls it a local notification.

Alarms, timers, medication reminders, habit prompts and “your session starts in ten minutes” are natural uses. So is the “come back” message from a game you have not opened for three days, which was scheduled the last time you closed it.

Local scheduling has three limits that produce visible behavior.

  • It knows only what the device knew when it was scheduled. A reminder for an event that you cancelled on another device still fires, unless a push wake or a deferred task reached this device to cancel it.
  • The platform caps the number pending. An app with many future reminders schedules only the nearest ones and tops them up each time it runs. Reminders far in the future stop firing if the app has not run for a long time.
  • A restart or a time zone change may clear or shift them. The app must reschedule when it next runs.

The alternative is a reminder scheduled on the backend and sent by platform push. It is correct across devices and fails offline. Probe P23.8 separates the two.

What the push service does while the device is unreachable

Section titled “What the push service does while the device is unreachable”

A device in airplane mode, switched off or out of coverage cannot receive a push. The platform push service keeps messages for it, within limits the sender can influence.

  • Expiry. The sender sets how long a message is worth delivering. A message past its expiry is dropped. “Your driver is outside” should expire in minutes. “You have a new message” can wait for days.
  • Replacement key. Messages with the same key replace each other while stored. Only the latest is delivered.
  • Storage limits. Some push services keep only one message, or a small number, per app per device. Others keep more and deliver them together.

Figure 23.3 shows a device that misses three events and reconnects.

Figure 23.3. Replacement and expiry while a device is unreachable, followed by a push wake that pulls everything.

The number of notifications that appear on reconnecting is a direct reading of these settings. One per event means no replacement key and a long expiry. One in total means replacement, or a push service that keeps only the latest. None means a short expiry, or a backend that did not send because it saw the device as unreachable.

The data is a separate matter. Whatever the notifications do, the app’s next pull by cursor brings all three events, as in Chapter 14.

23.8 What the outside view can and cannot tell you

Section titled “23.8 What the outside view can and cannot tell you”
  • Whether a notification appears in each app state is Observed.
  • System-displayed against app-built can be Inferred by applying a limit from Section 23.4 and comparing the notification with the one seen without the limit (P23.3 and P23.7).
  • A combined design shows as a notification with less detail under a limit. A notification that looks identical under every limit is system-displayed, or the fallback text happens to equal the built text. The second reading is Speculative and cannot be excluded.
  • Which grant ran a piece of background work is often not separable. A local store that moved forward while the app was unopened fits a deferred task and a push wake equally well. Low-power mode suppresses both.
  • Exit grace period against a transfer handed to the system cannot be told apart for small work. Both finish something after you leave.
  • Quota state is invisible. You can see that a wake did not happen. You cannot see why.

Record the app state for every observation. “Notification arrived” means little without “after a force-quit, in low-power mode, with the device locked”.

Most of these need a second device or a second account to cause events. Run P23.1 and P23.2 first, because they need nothing but a look. Run P23.3 before the others that withhold a wake, because it gives the comparison they use. Select the outcome you saw in each probe, and it is recorded in your worksheet for the teardown chosen here.

Open worksheet
Do
  1. Open the device settings for the app and go to its notification settings.
  2. Count the named categories that have their own switch.
  3. Open the app's own settings screen and look for notification preferences there.
  4. If you have a second device signed in to the same account, check whether a preference changed in the app appears there too.
Watch

Where the switches live, how many there are, and whether a preference follows the account to another device.

Read

Select what you saw to record it in your worksheet. Select it again to clear it.

Do
  1. Start something the app does over time, such as playback, a recording, turn-by-turn directions, a tracked activity or a large export.
  2. Go to the home screen and lock the device.
  3. Look at the lock screen and the status area for an indicator, controls or a persistent notification.
  4. Wait ten minutes, then check whether the work continued.
Watch

Whether the work continues, and what the system shows while it does.

Read

Select what you saw to record it in your worksheet. Select it again to clear it.

Do
  1. On device A, put the app in the background. From a second device or account, cause an event that notifies A. Take a screenshot of the notification.
  2. Force-quit the app on A.
  3. Cause the same kind of event again. Take a screenshot of the notification, if one arrives.
  4. Compare the two for text, sender name, image, grouping and actions.
Watch

Whether a notification arrives after the force-quit, and whether it has the same detail as the first.

Read

Select what you saw to record it in your worksheet. Select it again to clear it.

Do
  1. Open the same account on a second device, or have a second account that can see your changes.
  2. On device A, make a change that takes a moment to send, such as a post with a photo or a long message.
  3. Confirm it and go to the home screen at once. Do not return to the app.
  4. Watch the second device for two minutes.
Watch

Whether the change reaches the second device while the app on A stays in the background.

Read

Select what you saw to record it in your worksheet. Select it again to clear it.

Do
  1. Put the app on device A in the background and keep a second device or account in view.
  2. Cause an event that notifies A.
  3. Expand the notification on A and look for buttons or a reply field.
  4. Use one action that changes something, such as a reply or marking the item done. Watch the second device.
Watch

Whether the action completes without the app coming to the screen, and whether the result reaches the second device.

Read

Select what you saw to record it in your worksheet. Select it again to clear it.

Do
  1. Sign in to one account on two devices, A1 and A2. Put the app in the background on both.
  2. From another account, cause an event that notifies both. Confirm both show the notification and note the badge on each.
  3. Open the item on A1 only.
  4. Watch the notification and the badge on A2 for one minute without touching it.
Watch

Whether the notification and the badge on A2 change after the item is read on A1.

Read

Select what you saw to record it in your worksheet. Select it again to clear it.

Do
  1. Run P23.3 first and keep the screenshot of the notification received with the app in the background.
  2. Turn on low-power mode on device A. Open the app, then put it in the background and wait five minutes.
  3. From a second device or account, cause the same kind of event three times, a few minutes apart.
  4. Compare each notification with the screenshot. Then turn low-power mode off.
Watch

Whether notifications still arrive, and whether they have the same detail as without low-power mode.

Caution. This probe turns on low-power mode, which changes the behavior of every app on the device. Turn it off when you finish.

Read

Select what you saw to record it in your worksheet. Select it again to clear it.

Do
  1. Find a feature that notifies you at a time you choose, such as a reminder, an alarm or a scheduled alert. Set one for five minutes from now.
  2. Go to the home screen and turn on airplane mode.
  3. Wait until two minutes past the chosen time.
  4. Turn airplane mode off and wait another two minutes.
Watch

Whether the notification appears at the chosen time while the device is offline.

Caution. This probe uses airplane mode, which interrupts calls and other apps on that device.

Read

Select what you saw to record it in your worksheet. Select it again to clear it.

Do
  1. Put the app on device A in the background and turn on airplane mode.
  2. From a second device or account, cause five events that would each notify A, one minute apart. Number them if the event has text.
  3. Wait five more minutes, then turn airplane mode off on A. Do not open the app.
  4. Count the notifications that arrive in the next two minutes and note which events they name.
Watch

How many notifications arrive, and whether they are separate, merged or only the latest.

Caution. This probe uses airplane mode, which interrupts calls and other apps on that device.

Read

Select what you saw to record it in your worksheet. Select it again to clear it.

Do
  1. Open the app and note the newest item on its first screen. Leave the app without a force-quit.
  2. Do not open the app for at least three days. Use the device normally and charge it overnight. Dismiss its notifications without tapping them.
  3. Make sure the account has new content by then, from a second device or account if needed.
  4. Turn on airplane mode, then open the app.
Watch

Whether the first screen holds content newer than the item you noted, while the device is offline.

Caution. This probe uses airplane mode, which interrupts calls and other apps on that device.

Read

Select what you saw to record it in your worksheet. Select it again to clear it.

SignalDesign it points toConfidence
A persistent indicator or lock-screen controls while the app is in the backgroundUser-visible work declared to the operating systemObserved
Several named notification categories in the system settingsEvent types declared to the platform and muted on the deviceObserved
A notification preference changed in the app also applies on a second devicePreferences stored on the backend and applied before sendingInferred
The same notification arrives after a force-quitSystem-displayed notification with text written by the backendInferred
A plainer notification after a force-quit or in low-power modeApp-built notification with fallback text in the payloadInferred
No notification after a force-quitApp-built notification that depends on a push wakeInferred
A notification shows a contact name that exists only on this deviceApp-built notification composed from local dataInferred
Readable message text in a notification from an app with end-to-end encryptionContent decrypted on the device by a short-lived process before displayInferred
A notification image that is missing on a weak networkImage downloaded on the device under a deadline before displayInferred
A reply sent from a notification without the app openingNotification action that runs a mutation in a short background windowInferred
A notification disappears after the item is read on another deviceWithdrawal push handled by a push wakeInferred
The badge changes while the app is force-quitBadge count computed on the backend and carried in the payloadInferred
The badge disagrees with the unread count inside the appTwo sources for the count, one on the backend and one on the clientSpeculative
A notification changes its text in place instead of adding a new oneReplacement key shared by successive pushesInferred
A reminder fires on time in airplane modeLocal notification scheduled on the deviceInferred
A reminder fires for something already completed on another deviceLocal notification that no wake reached to cancelInferred
One notification after a long offline period in which several events happenedReplacement key, or a push service that stores only the latest messageSpeculative
Fresh content offline after days without opening the appDeferred task or push wake that refreshes the local storeInferred

Search the signals from every chapter in the signal lookup.

Each client design needs specific backend support. Once you have placed a feature, the following can be Inferred.

  • Any platform push. A store of device tokens per user and per device, refreshed when the platform issues a new token, and cleared on sign-out (Chapter 20). Handling of the push service’s reply that a token is dead.
  • System-displayed notifications. A notification service that writes final text. It needs the user’s language, the sender’s display name and the formatting rules, all on the backend. Text that appears in the wrong language after you change the device language shows where the language is stored.
  • App-built notifications. A payload with identifiers only, and a read endpoint fast enough to answer inside a push wake. For end-to-end encryption, a payload that carries ciphertext.
  • Preferences applied before sending. A per-user, per-category preference record, quiet hours with the user’s time zone, and rate limits so that a burst of events becomes one notification.
  • Withdrawal. Read state fanned out to the user’s other devices as its own push, as in Chapter 14.
  • Backend badge counts. An unread counter per user, updated on every event and every read, and included in each payload.
  • Updating in place. A stable replacement key per thread or per item, stored with the item.
  • Scheduled sends. A job scheduler on the backend that knows each user’s time zone. Notifications that arrive at the same local hour for everyone were sent in waves by zone.
  • Deferred refresh. A cheap delta endpoint, because a deferred task has little time and may run on a metered network.
  • Delivery feedback. Many backends record whether a notification was displayed or opened, reported by app code. This feeds the rate limits, and it is one reason an app prefers the app-built path.
  • Notifications stop after a force-quit and return after you open the app. The feature depends on a push wake, with no fallback text in the payload.
  • A notification with a generic line such as “You have a new message”. Fallback text. The wake that would have built the full notification did not run, or the content is unreadable to the backend.
  • A notification arrives without its image on a weak network. The image is downloaded on the device before display, under a deadline.
  • A notification stays on the tablet after you read the item on the phone. No withdrawal push, or the withdrawal’s wake was refused.
  • The badge shows a number and the app shows nothing unread. Two sources for the count, one on the backend and one on the client.
  • The badge never clears until you open a specific screen. A client-computed badge that is recalculated only by that screen.
  • A reminder fires for something you already completed on another device. A local notification scheduled before the change, and no wake reached this device to cancel it.
  • Reminders stop after a few weeks of not opening the app. The platform’s cap on pending local notifications, with top-up only when the app runs.
  • A burst of stale notifications when you leave airplane mode. No replacement key and a long expiry.
  • “Your order is arriving” appears an hour after it arrived. No expiry on a time-bound message.
  • A reply typed in a notification never arrives. The action was sent directly inside the background window, with no outbox, and the request failed.
  • The app opens to old content every morning in low-power mode and to fresh content otherwise. Refresh depends on deferred tasks or silent pushes.
  • Playback or tracking stops a few minutes after the screen locks. The activity was not declared as user-visible work, or the process was removed and the work was not resumed.
  • The same notification twice. The backend sent a system-displayed push and the app also built one, with different identifiers.
  • Notifications continue for an account you signed out of. The device token was not removed from the backend at sign-out.

These are the fields this chapter adds to the teardown worksheet. Probe outcomes fill some of them in. Complete one set per feature that notifies or works in the background.

Open worksheet
  • The operating system grants background execution in four kinds: an exit grace period on leaving, a deferred task, user-visible work and a push wake. Only user-visible work lasts, and it always shows an indicator.
  • Quotas, low-power mode, a force-quit, long disuse, user settings and a restart before first unlock all withhold push wakes and deferred tasks. None of them stops a system-displayed notification.
  • A notification is written either by the backend and displayed by the system, or built by app code after a wake. Many apps send fallback text and replace it when the wake runs.
  • Applying a limit and comparing the notification is the main test. A notification that loses detail or disappears is app-built.
  • Grouping, actions, images, in-place updates, withdrawal, badge counts and mutable categories each expose a part of the payload, the wake or the backend.
  • A local notification needs no backend and fires offline. It knows only what the device knew when it was scheduled.
  • While a device is unreachable, expiry and replacement keys decide how many notifications appear on reconnecting. The data arrives by a pull regardless.
  • Which grant ran a piece of background work is often not separable from outside. Record the app state with every observation.