A push notification is an interruption you have not yet earned. Get it right and it pulls a lapsed user back to the exact thing they cared about. Get it wrong and it is the fastest uninstall trigger you own. The gap between those two outcomes is not the sending infrastructure, which is mostly solved, it is the judgment around permission, timing, relevance, and where the tap actually lands. This is a practitioner's guide to shipping mobile push notifications people opt into and act on.
The TL;DR
- Push works when it earns the opt-in, arrives with genuine intent, and routes to the right screen. It fails when it is generic, mistimed, or dumps the user on a home screen with no memory of why they tapped.
- iOS and Android differ at the OS layer: Apple's APNs requires explicit permission before a single notification can show, while Android grants it by default before Android 13 and prompts from 13 onward. Design for both.
- Ask for permission in context, not on first launch. A pre-permission priming screen that explains the value first can double opt-in rates versus firing the system prompt cold.
- Segmentation and behavioral triggers beat broadcast blasts on every metric that matters: open rate, conversion, and retention. A relevant push to 10% of users outperforms a generic push to 100%.
- Measure the full funnel, not just delivery. Delivered, opened, and converted are three different numbers, and the gap between opened and converted is usually a broken deep link.
What makes a push notification worth sending?
A push notification is worth sending when it delivers value the user would have missed otherwise, at a moment they can act on it. That is the entire test. Everything else - copy, timing, channel - flows from whether the message clears that bar.
The failure mode is treating push as a free broadcast channel. It is not free: every irrelevant notification spends trust, and trust does not refill. A user who mutes your app after three bad pushes is worse off than one you never notified, because you have lost the channel permanently. The cost of a bad push is not zero engagement, it is negative.
Concretely, a worthwhile push falls into one of a few buckets: it is transactional (an order shipped, a message received, a payment cleared), it is a genuine re-engagement trigger tied to the user's own behavior (an abandoned cart, a match, a price drop on a watched item), or it is time-sensitive information the user explicitly asked to track. A generic "we miss you" blast is none of these, which is why it converts so poorly. If you cannot name the specific value and the specific moment, do not send it.
How do APNs and FCM actually differ?
APNs and FCM differ most in the permission model and in what the platform guarantees about delivery. Both are the transport layer that carries a payload from your server to a device, but the rules around them are not symmetric, and a setup that assumes they are will silently underperform on one platform.
Apple Push Notification service (APNs)
On iOS, nothing shows until the user grants permission. Your app must call the notification authorization API, the system shows a one-time prompt, and if the user declines you get no second chance at the system dialog. APNs itself is reliable and low-latency, but it is best-effort: a device that is offline receives only the most recent notification for a given collapse identifier when it reconnects, not the full backlog. Apple's User Notifications documentation is the source of truth for the authorization flow and payload limits.
Firebase Cloud Messaging (FCM)
FCM is the common send path for Android and, increasingly, a unified API that also brokers to APNs for iOS so you maintain one server integration. On Android, notification permission was implicit before Android 13; from Android 13 (API level 33) onward, apps must request the POST_NOTIFICATIONS runtime permission, which brought Android closer to the iOS model. FCM distinguishes notification messages (handled by the system tray automatically) from data messages (delivered to your app for custom handling), and that distinction matters for background behavior. Google's Cloud Messaging documentation covers message types, priority, and platform-specific overrides.
The practical takeaway: build one send abstraction, but do not assume identical behavior. Test permission flows, background delivery, and payload rendering on both platforms separately. If your stack is React Native or Expo, Expo's push notifications overview wraps both services behind a single token, which removes a lot of the divergence for teams that do not need raw platform control.
When should you ask for permission?
Ask for notification permission in context, at the moment the value is obvious, never on first launch. The single most expensive push mistake teams make is firing the system permission prompt during onboarding, before the user understands what notifications will do for them. On iOS that prompt fires once, so a cold "no" is permanent.
The pattern that works is a pre-permission priming screen: a soft, in-app screen you fully control that explains the specific benefit ("Get notified the moment your order ships") and offers a "Turn on notifications" button. Only when the user taps that do you trigger the real system prompt. Because you asked at a relevant moment and the user already said yes to your screen, the system-level opt-in rate climbs sharply. If the user declines your soft prompt, you have lost nothing, the system dialog stays unfired and available for later.
Timing the priming screen is its own decision. Tie it to a moment of demonstrated intent: after a first purchase, after a user follows someone, after they save an item. That intent is the strongest possible context, and the ask feels like a feature rather than an interruption. Designing that moment well is a UX problem as much as an engineering one, which is why our UX/UI design practice treats the permission flow as part of the core experience, not a checkbox.
How do you segment without being creepy?
You segment by behavior and stated preference, and you stay on the right side of creepy by using only data the user would expect you to have. Segmentation is what separates push that retains from push that repels, but it has a failure mode: personalization that reveals you are tracking more than the user realized reads as surveillance, not service.
The durable approach is behavioral and consent-aware. Segment on first-party signals the user generated inside your product: items they viewed, actions they completed, categories they follow, where they dropped off. Trigger on those signals rather than on a calendar. An abandoned-cart reminder two hours after the fact converts because it is relevant and expected; a push referencing something the user did on another site does not, and it corrodes trust.
A few rules keep segmentation effective and honest:
- Trigger on the user's own actions, not on inferred identity. "You left three items in your cart" is fair game. "We noticed you browsed competitors" is not.
- Cap frequency per segment. Even relevant notifications fatigue. A hard ceiling (for example, no more than a few per week for non-transactional pushes) protects the channel.
- Respect quiet hours and time zones. A 3am notification is a mute button regardless of relevance.
- Let preferences be granular. Users who can turn off marketing push but keep transactional push mute far less often than users facing an all-or-nothing switch.
What does the send-and-route pipeline look like?
The pipeline is: your backend decides a push is warranted, sends a payload with routing data through FCM or APNs, the device receives it, and on tap the app reads that routing data and navigates to the exact screen. The last step is where most teams leak conversions, because a notification that opens the home screen has thrown away the intent that made the user tap.
On the server, a NestJS service sending through the Firebase Admin SDK keeps the routing payload explicit:
// src/notifications/push.service.ts
import { Injectable } from '@nestjs/common';
import { getMessaging, type Message } from 'firebase-admin/messaging';
interface OrderShippedPush {
deviceToken: string;
orderId: string;
title: string;
body: string;
}
@Injectable()
export class PushService {
async sendOrderShipped(input: OrderShippedPush): Promise<string> {
const message: Message = {
token: input.deviceToken,
notification: { title: input.title, body: input.body },
// data travels with the notification and drives in-app routing
data: { screen: 'order', orderId: input.orderId },
apns: { payload: { aps: { sound: 'default' } } },
android: { priority: 'high' },
};
// returns the provider message id; log it to reconcile delivery later
return getMessaging().send(message);
}
}
On the device, a React Native handler reads the same data payload and routes, failing soft if anything is missing:
// notifications.ts (React Native / Expo)
import * as Notifications from 'expo-notifications';
import { router } from 'expo-router';
export function initPushRouting(): () => void {
const sub = Notifications.addNotificationResponseReceivedListener((response) => {
const data = response.notification.request.content.data as {
screen?: string;
orderId?: string;
};
// fail soft: unknown or missing routing lands on a sensible default
if (data.screen === 'order' && data.orderId) {
router.push(`/order/${data.orderId}`);
} else {
router.push('/');
}
});
return () => sub.remove();
}
Two things matter. The routing data rides with the notification so the tap always knows where to go, and the handler degrades gracefully when it does not recognize the payload. When the destination lives behind an app install or a link tapped outside the app, this handoff becomes deferred deep linking, which is a harder problem with its own privacy constraints; we covered that end to end in our research on mobile deep linking with Branch. The send infrastructure itself - token storage, retries, provider reconciliation - is standard backend work that our backend and cloud practice wires up alongside the rest of the API.
How do you measure push: delivery, open, conversion?
You measure push across three distinct numbers - delivered, opened, and converted - because the drop between each stage points at a different problem. Teams that track only "sent" are flying blind; sent is not delivered, and delivered is not acted on.
- Delivered is the count that actually reached a device. A low delivered-to-sent ratio points at stale or invalid tokens, users who revoked permission, or platform throttling of low-priority messages. Prune dead tokens regularly.
- Opened is taps divided by delivered. A weak open rate is usually a relevance or copy problem: the message did not clear the "worth sending" bar, or the first few words did not signal value.
- Converted is the count who completed the intended action after tapping. A healthy open rate with poor conversion almost always means a broken or generic destination, the deep-link handoff from the previous section is not landing them where the notification promised.
Watch a fourth number that never appears in the send dashboard: the opt-out and mute rate. Rising mutes after a campaign is the clearest signal you are overspending trust, and it predicts churn better than any single-send metric. Instrument all four, review them per segment, and let the numbers, not intuition, decide send frequency.
What are the most common push mistakes?
The failures cluster in a handful of predictable places, and knowing them up front saves a painful round of lost trust after launch.
- Asking for permission on first launch. The prompt fires before the user understands the value, the answer is often no, and on iOS that no is permanent. Prime in context instead.
- Broadcasting to everyone. A generic blast to the full base tanks relevance and spikes mutes. Segment or do not send.
- Routing every tap to the home screen. The notification created intent and the destination discarded it. Carry routing data in the payload and honor it.
- Ignoring frequency and quiet hours. Even good notifications fatigue and a badly timed one is a mute button. Cap frequency and respect the clock.
- Treating transactional and marketing push as one setting. Users who cannot keep order updates while muting promotions will mute everything. Split the preferences.
- Never pruning tokens. Sending to dead tokens inflates your "sent" number, wastes quota, and hides real delivery problems behind noise.
Privacy, consent, and compliance
Push routing itself is low-risk, but the targeting and analytics behind it process personal data, so treat the notification system like any other tracking dependency. The message "your order shipped" needs no special consent; the behavioral profile that decides a user should receive a marketing push does, because it processes activity and device identifiers.
The practical obligations: gate non-essential, marketing-oriented notifications behind explicit consent separate from transactional ones, document what behavioral data you store and for how long, and make opting out as easy as opting in. Under GDPR, marketing push generally requires a lawful basis and a clear opt-out; conflating it with transactional messaging is a common compliance gap. On iOS, App Tracking Transparency governs cross-app tracking identifiers, so any segmentation that reaches beyond your own first-party data needs a hard look. When in doubt, prefer first-party behavioral signals, which are both more effective and less fraught, and confirm your provider's data-processing terms against your own requirements before launch.
Frequently asked questions
Do users have to opt in to push notifications on both iOS and Android?
On iOS, yes, always: nothing shows until the user grants permission, and the system prompt fires only once. On Android, notifications were granted by default before Android 13; from Android 13 onward apps must request the runtime notification permission, so both platforms now require an opt-in on current OS versions. Design for an explicit yes on both.
What is a good push notification opt-in rate?
It varies widely by category, but the lever you control is the priming pattern. Firing the system prompt cold on first launch produces markedly lower opt-in than asking in context after a moment of demonstrated intent, fronted by a soft pre-permission screen. Teams that adopt in-context priming commonly see system-level opt-in rise substantially versus a cold prompt. Focus on the pattern, not a universal benchmark.
Should I use notification messages or data messages in FCM?
Use notification messages when you want the OS to display the alert automatically with no custom logic, and data messages when your app needs to handle the payload itself, for example to control exactly when and how it renders or to drive in-app routing. Many production apps send a combined payload so the system displays the alert and your app still receives the routing data on tap.
Why do notifications open the home screen instead of the right page?
Because the routing information is not being carried in the payload or is not being read on tap. The fix is to include destination data in the notification (a screen identifier and any needed IDs) and to handle the notification-response event in the app to navigate there. When the destination is behind an install, you need deferred deep linking, which is a separate, harder problem.
How often is too often for push notifications?
There is no single number, but the signal to watch is your mute and opt-out rate. When it rises after a campaign, you are sending too much for that segment. Cap non-transactional notifications to a small weekly ceiling, respect quiet hours and time zones, and let per-segment mute rates set the pace rather than a fixed cadence for everyone.
Sources and further reading
- Apple - User Notifications framework and APNs: https://developer.apple.com/documentation/usernotifications
- Firebase - Cloud Messaging documentation: https://firebase.google.com/docs/cloud-messaging
- Expo - Push notifications overview: https://docs.expo.dev/push-notifications/overview/
What's next
Push notifications reward restraint. The version that retains users earns the opt-in in context, sends only when there is real value at a moment the user can act, routes the tap to the exact screen that value lives on, and watches the mute rate as closely as the open rate. Get those four right and push becomes one of the highest-leverage retention channels you own; get them wrong and it is the fastest way to lose a user for good. Choosing which platform to build for first shapes a lot of this - our take is in iOS vs Android first, and it fits into the broader picture of mobile app development. If you are wiring up push and want it done without leaking trust, get in touch.