Push notifications: sent, delivered and read are different events

Why a successful FCM or APNs response is not enough. Designing a notification history, message expiry, device registrations and delivery measurements for iOS and Android.

Close-up of a mobile phone displaying app icons

Separate message acceptance from someone reading it

A town publishes a change to the waste collection schedule and its server sends a push message. The administration screen shows success. Some phones have no signal, some residents have disabled notifications and others will see the alert after work. Marking everyone as informed would claim something the system cannot prove. A provider's technical response and a person's behaviour are separate events.

Firebase explicitly distinguishes accepting a request from delivery to a device. Delivery reports also have their own coverage and limitations. Keep separate states for scheduled sending, provider acceptance, an event recorded by the app, an opening and, where necessary, an explicit user confirmation. Only mark the last three when you have a corresponding event. A missing event does not automatically mean the person never saw the notice.

RecordWhat it establishesWhat it does not establish
Provider acceptanceA valid sending attemptThe phone displayed an alert
App records the messageApplication code processed an eventThe user read the content
Detail openedThe user opened that contentThey understood it or acted on it
Explicit confirmationThe user confirmed a particular versionDelivery of subsequent updates

The notice must exist without its push message

Store the durable notice on the server with an ID, version, publication time and optional expiry. The push carries a hint and an identifier that lets the app retrieve the current detail. When someone opens the list, the app checks for new notices even if no push arrived. After a connection failure, the phone can then find the content and reconcile its stored data with the server.

An outbox can create the notice and its sending task in one server-side transaction. A worker sends individual attempts and records provider responses. This is a proposed example architecture; push services themselves are not a durable queue of every business event. An explicit acknowledgement requirement needs a separate process inside the application. When evidence is missing, report an unknown state instead of inventing confirmation.

Choose message lifetime to match the content

FCM supports a TTL; zero means a message that cannot be delivered immediately is discarded. A collapse key can replace an older pending message with a newer one. Apple platforms use apns-expiration, which APNs attempts to honour without an absolute guarantee. Choose values according to the situation and the server's current time.

An invitation to today's event needs a different lifetime from a notice about a new document. If the content has expired, the app should show its current state even when an alert arrives late. Collapsing messages suits a hint that a list has changed, because the app fetches all changes. For individual submissions that people need to follow separately, keep your own history and decide whether merging alerts would lose meaning.

Illustrative FCM HTTP v1 payload for a list refresh; the registration token and absolute expiry are placeholders.json
{
  "message": {
    "token": "<registration-token>",
    "data": {
      "type": "notices-changed",
      "noticeId": "notice-84",
      "version": "3"
    },
    "android": {
      "ttl": "3600s",
      "collapse_key": "notices"
    },
    "apns": {
      "headers": {
        "apns-push-type": "background",
        "apns-priority": "5",
        "apns-expiration": "<future-unix-seconds>"
      },
      "payload": {
        "aps": { "content-available": 1 }
      }
    }
  }
}

Silent updates are subject to operating system restrictions

A silent background push on iOS does not display a visible alert. Apple can delay or throttle its delivery and does not guarantee that it will arrive. The example payload is therefore a hint to refresh data, rather than a reliable way to distribute visible notices. Alerting a person requires a different alert payload and the corresponding platform configuration.

Break the refresh into small, repeatable steps. When the app wakes, it checks its last stored version and retrieves changes if needed. It does not have to download every photograph, archive and supporting file in one run. Use the same procedure when the app opens. Also decide what people see before the refresh finishes: stored content with its update time, or a short loading state for a detail that is not yet available locally.

A registration identifies an installation, not a permanent contact

Device registrations can change, and the provider may report that a registration is no longer valid. Record the current registration supplied by the app and its update time. A useful model includes your own installation ID, platform, environment, account association and subscriptions. One person may use several devices, and different people may sign in on the same device.

At logout, remove personal subscription associations; an anonymous subscription to public notices may legitimately remain. Restore personal subscriptions at login only after checking identity. Receiving a push must not decide whether someone can access a private detail. The server checks permission when returning the content. Avoid putting sensitive information on the lock screen if the user would not want a nearby person to see it.

Distinguish errors that warrant a retry

The provider may reject a malformed payload, the wrong environment, invalid credentials or an invalid registration. These outcomes require different responses. Do not resend a formatting error without correcting it; retry a provider outage using the agreed delay policy. Stop using a target when the provider confirms its registration is invalid. A general request error alone is not enough to delete the registration.

Associate every attempt with the notice ID, installation ID and an internal send ID. A retry may produce more than one alert, so the client should compare ID and version when storing an event. Logs need results and timestamps, rather than copies of all personal information or complete registration tokens. Agree retention according to the operational purpose and limit diagnostic access to people investigating specific problems.

Measurements need an explicit denominator

Accepted requests, devices with an active subscription and detail openings can produce three different percentages. Label each chart with the events being compared and the time window. Firebase warns that its metrics do not cover every scenario. Your own application telemetry can also be missing on offline devices or arrive later.

If an event uploads at the next launch, distinguish the event time from its arrival in analytics. Separate opening through a push link from opening through the list, or you may attribute the outcome to the wrong channel. Before tracking more events, identify the decision the data will inform. Investigating delays may require only a small set of operational events rather than a detailed record of everything a person does.

Test missing delivery, expiry and account changes

Testing on one unlocked phone connected to Wi-Fi covers only an easy path. The test plan needs real iOS and Android devices, different notification settings and separate production and test registrations. Define success as the behaviour of a particular feature, rather than a successful HTTP response. These cases are proposed checks, not claims about measurements from deployed applications.

  • The phone reconnects after an event ends: the app displays the current detail and a delayed message cannot make the invitation valid again.
  • The person disables notifications: the notice stays available in the list without a fabricated read receipt.
  • A silent push never arrives: opening the app retrieves missing changes through the same synchronisation process.
  • The server repeats an attempt: local history has one entry for the same ID and version.
  • One person logs out and another signs in: personal subscriptions and access to the detail follow the new identity.
  • The provider rejects a registration or credential: the system distinguishes retiring a target from correcting server configuration.

Sources and documentation

For implementation, consult the documentation for the version you use.

Put the topic into practice.

Related project: Municipality of Smolenice

Have a process
that needs to change?

Let’s start with how you work today. We’ll choose the technology around it.

Discuss your project