Mobile Computing · Mobile Networking

Push Notification as a Store-and-Forward Channel

Why mobile push cannot be a socket the server writes to, how the vendor service buffers and delivers, and what the delivery guarantees do and do not include.

The intuition that a server holds a socket to each phone is wrong in a way that shapes every mobile messaging design. No application server has a connection to a device. Between them sits the platform vendor’s push service, which is the only party with a persistent relationship to the device, and it holds that relationship at the level of the operating system rather than of any application.

The architecture is store-and-forward, and it resembles a mail system more than a telephone call. A server hands a message to the vendor; the vendor stores it; the vendor delivers it when the device is reachable; and the delivery may fail, may be delayed, may be duplicated, and may be discarded. Understanding which of those are guarantees and which are not is the difference between a design that uses push as a notification channel and one that mistakenly uses it as a data transport.

Why the vendor sits in the middle

A device that is idle has no radio bearer and no address that a server could send to. Its reachability comes from paging, described in Radio resource control states and the tail timer after the last byte, and paging is a function of the core network, not of the internet. There is no route from an arbitrary server to an idle phone, because the phone is not on the internet at that moment.

Maintaining a connection per application would solve this for the application and break it for the device. Ten applications each holding a persistent connection would keep the radio active nearly continuously, which is precisely the energy cost the state machine exists to avoid. The platform’s answer is one connection, owned by the operating system, shared by every application, and used to carry a small message that tells an application to wake and fetch its own data.

That single connection is the reason push exists as a platform service rather than as a library. It is also the reason a third-party cannot build an equivalent: the ability to wake an application that is not running is a privilege the operating system grants to its own push path.

The path a message takes

An application server sends a request to the vendor’s HTTP endpoint, authenticated with a key issued to the application’s developer account. The request names the device with a token that the device obtained earlier from the vendor and registered with the application server. The vendor’s response reports acceptance or rejection, with a status code that distinguishes a malformed request from an expired token from a rate limit.

Acceptance is not delivery. The vendor then holds the message until the device is reachable, and the holding is the store in store-and-forward. When the device next has a radio bearer, or, for a notification that can wait, when it next wakes for its paging occasion, the vendor delivers, and the operating system decides what to do with it.

For a notification that should be shown to the user, the operating system displays it without involving the application at all. For a message that should cause work, the operating system may launch the application in the background for a bounded period, or may deliver to an extension process with a smaller budget. The application never controls this decision; it can only declare what kind of message it is sending and hope the platform’s policy allows the corresponding treatment.

The token is the addressing mechanism and it changes. A reinstall, a restore to a new device, and in some cases an operating system update produce a new token, and the old one becomes invalid. A server that stores a token once and never refreshes it accumulates addresses that the vendor rejects, and the rejection is the only signal it gets. Treating token invalidation as a first-class lifecycle event, which means removing the token on the vendor’s rejection response and re-registering on every application start, is the minimum for reliable addressing.

Payload size and what it forces

Push payloads are small. The limits are set by the vendor and by the transport, and the practical constraint is a few kilobytes rather than a few hundred. Apple’s notification payload limit is 4 KB for the remote notification path. The exact figures differ between vendors and have changed over time; the design consequence is stable regardless: a push payload cannot carry the data a screen needs.

This is why the common pattern is a content-free notification: the payload says that something happened and perhaps names what, and the application, when it wakes, fetches the actual data over its own connection. The fetch is a normal request, subject to the round-trip budget described in Counting round trips before the first payload byte, and it is where the real cost of the notification lies: a push that wakes a device to make a request costs a radio transition plus a request, and a push that is merely displayed costs only the delivery.

The alternative, putting content in the payload, is possible for small content and has a specific advantage: the notification can be rendered by the operating system while the application is not running, so the user sees the message without any application work. That advantage is why messaging applications put message text in the payload, and why they still need a fetch path for history and for attachments.

Delivery is not guaranteed, and the guarantee is not the one expected

The vendor services document their behaviour as best-effort. A message may be delivered more than once, and an application that increments a counter on every receipt will over-count. A message may be delayed until the device is reachable, which may be hours. A message may be discarded: when it has expired under the vendor’s time-to-live, when the device’s token is invalid, or when the platform’s policy decides the application has used its allowance.

The duplication is the property that most designs get wrong, because it is invisible in testing on a single device with a good connection. The defence is the same as everywhere else in this cluster: make the handling idempotent. A notification that carries an event identifier lets the receiver discard a repeat. A notification that carries only “something changed” is naturally idempotent as long as the fetch that follows is a read.

Ordering is also not guaranteed. Two messages sent in sequence may arrive out of order, particularly if one is delayed and the other is not. A design that relies on the arrival order of pushes to determine state will occasionally apply them backwards, and the fix is to include a sequence or a timestamp in the payload and to discard anything older than what has already been applied.

There is a stronger guarantee available for messages that must not be lost, and it is not push. It is the application’s own queue: the server writes the event to durable storage, and the client fetches it on its own schedule, with push used only as a hint that there is something to fetch. That architecture makes the push channel’s unreliability irrelevant, because losing a hint costs latency rather than data. It is the same relationship as between a doorbell and a letterbox, and the reason every serious messaging system ends up with both.

Silence is not a signal

The absence of a push says nothing. It may mean nothing happened, or it may mean the message was dropped, or the token was invalid, or the device was unreachable past the expiry, or the platform throttled the application.

Applications that infer state from the absence of a notification produce bugs that appear only on some devices and some networks. A client that waits for a push before refreshing will eventually sit on stale data indefinitely, and the only defence is a fallback: refresh on foreground, refresh on a schedule with a wide interval, and treat the push as an optimisation that usually makes the refresh unnecessary.

The reverse inference is also unsound. Receiving a push does not mean the server considers the device up to date, because the push may have been generated before other changes, and it does not mean the application’s own queue is empty. The push is one event in a stream that the client must reconcile against its own state, which is why the reconciliation described in Replaying a queue against state that has moved on applies to the fetch triggered by a push as much as to a write composed offline.

Background delivery and the budget

When a push causes the operating system to launch an application in the background, the launch comes with a budget. The platform limits how long the application may run, how often it may be launched this way, and how much network and energy it may consume. The limits are policy, not protocol, and they are not published as constants an application can query; they are enforced and observable only through the application’s failures.

Two consequences follow. First, the work done in response to a push must be small and must be able to complete or be abandoned cleanly, because the application may be suspended at any point. Second, the work must be resumable, which means it must be expressed in the durable-queue terms of The durable outbox rather than as a sequence of in-memory steps. A background launch that begins a multi-step fetch and is suspended halfway leaves nothing behind unless each step’s result was committed.

The platform’s throttling also interacts with the radio state machine in a way that is easy to miss. A background launch that makes one request pays a full radio transition, and a design that sends several pushes to trigger several small fetches pays several transitions. Coalescing, meaning one push that says “there are five things” rather than five pushes, is cheaper on every axis: the vendor’s quota, the radio, and the user’s attention.

What push is for

The channel’s properties define its proper use: it is a low-latency, unreliable, small, best-effort hint that something has changed, delivered through a party the application does not control, with no ordering and possible duplication.

Used that way, it removes polling and its radio cost, and it gives the user timely notifications without keeping the application running. Used as a reliable message transport, it produces a system that loses messages under conditions the application cannot observe, and the loss is discovered only when a user reports that something did not arrive. The fix in that case is never a better use of push; it is a durable server-side log and a client that can fetch from it.

References

  1. RFC 8030: Generic Event Delivery Using HTTP Push — IETF
  2. Apple Developer: Remote notifications — Apple
  3. Firebase Cloud Messaging: Message types — Google