Dec 18, 2025·7 min read

APNs vs FCM for iOS and Android push notifications

APNs vs FCM comparison for iOS and Android: token lifecycle, payload limits, delivery expectations, and a practical checklist to fix missing pushes.

APNs vs FCM for iOS and Android push notifications

What you are comparing (and why it matters)

APNs (Apple Push Notification service) and FCM (Firebase Cloud Messaging) are the delivery pipes that move a message from your server to a phone. They don’t decide what your app does with the message, but they strongly influence whether the message arrives, how quickly it arrives, and what shape it must have.

When people say a push notification “works on Android but not on iOS” (or the other way around), it’s rarely one single bug. iOS and Android handle background work, power saving, permissions, and message priority differently. The same message can be delayed, replaced by a newer one, shown without sound, or never displayed if the app can’t wake up to process it.

This comparison focuses on the parts that cause the most real-world surprises: how device tokens change over time, how big your payload can be and how it should be structured, what delivery you can realistically expect, and the common reasons notifications seem to go missing.

This does not cover choosing a push provider UI, marketing strategy, or building a full analytics pipeline. The goal here is reliability and faster debugging.

A few terms used throughout:

  • Token: a device-specific address you send to, issued by APNs or FCM.
  • Topic: a group address (mainly used with FCM) where many devices subscribe.
  • Channel: an Android notification category that controls sound, importance, and behavior.
  • Collapse key: a way to replace older pending messages with a newer one.
  • TTL (time to live): how long a message can wait for delivery before it expires.

Getting these basics right saves hours of guessing when a “simple push” behaves differently across iOS and Android.

How APNs and FCM work at a high level

APNs and FCM are both middlemen between your server and a user’s phone. Your app can’t reliably deliver a push notification directly to a device over the internet, so it hands that job to Apple (APNs) or Google (FCM), which already maintain trusted connections to their devices.

The overall flow is similar: your app gets a token, your backend sends a message to the push service using that token, and the push service routes it to the device.

APNs in plain terms

On iOS, the app registers for remote notifications and (usually) asks the user for permission. Apple then provides a device token. Your backend (often called your “provider”) sends a push request to APNs that includes that token and your payload. APNs decides whether it can deliver and forwards the notification to the device.

Your backend authenticates to APNs, typically using token-based auth (a signing key). Older setups use certificates.

FCM in plain terms

On Android, the app instance registers with FCM and receives a registration token. Your backend sends a message to FCM, and FCM routes it to the correct device. Depending on app state and message type, FCM may display a notification automatically or deliver data to the app for handling.

Your backend authenticates to FCM using server credentials (API key or service account).

What you control: app code, when you request permission, token storage, backend logic, and the payload you send. What Apple and Google control: the delivery network, reachability, throttling rules, and many last-mile conditions like power-saving and system policies.

Token lifecycle: how tokens are issued, refreshed, and invalidated

The biggest day-to-day difference in APNs vs FCM is that tokens are not “set once forever.” Treat them like addresses that can change without warning.

On iOS, the APNs device token is tied to the device, your app, and your Apple developer setup. It can change after an app reinstall, a device restore, certain OS updates, or when switching push environments (sandbox vs production) during development.

On Android, the FCM registration token can refresh when the app is restored on a new device, the user clears app data, Google rotates the token, or the app is reinstalled. Your app should expect refresh events and send the new token to your server promptly.

A simple rule: always upsert tokens, never “insert and forget.” When you store tokens, keep enough context to avoid duplicates and wrong targets:

  • User or account ID (if applicable)
  • App bundle/package and environment
  • Platform (iOS/Android)
  • Token value and last-seen timestamp
  • Opt-in status (permission granted/denied)

Deletes matter too. You usually learn a token is dead from delivery errors, not from a clean “uninstall” signal. If APNs returns an error like Unregistered (often with a 410 status), or FCM says NotRegistered/Unregistered, remove that token immediately so you stop retrying forever.

One easy way to leak private updates: a customer logs out and another logs in on the same phone. If you don’t clear or remap the token on logout, you can send notifications to the wrong person even though delivery “works.”

Payload constraints and message structure differences

The biggest practical difference in APNs vs FCM is what you can fit in a message and how the phone treats it when it arrives.

Most teams rely on a small set of fields:

  • Title and body text
  • Badge count (iOS)
  • Sound (default or custom)
  • Custom key-value data (for example, order_id, status)

Size limits: keep the push small

Both services have payload size limits, and the limit includes your custom data. When you hit the cap, delivery can fail or the message won’t behave the way you expect.

A reliable pattern is to send a short notification plus an ID, then fetch details from your backend:

Example: instead of sending a full order summary, send { "type": "order_update", "order_id": "123" } and let the app call your API to load the latest status.

Data-only vs notification behavior

On Android, an FCM message with a “notification” payload is typically displayed by the system when the app is in the background. A data-only message is handed to your app code, but it may be delayed or blocked by background limits and battery settings.

On iOS, alerts (title/body) are straightforward, but background updates are stricter. A background push isn’t a guarantee your code will run immediately. Treat it as a hint to refresh, not a real-time job trigger.

If you need reliability, keep the payload minimal, include a stable identifier, and design your app to reconcile state when it opens or resumes.

Delivery expectations and what can stop a notification

Generate native apps for both
Ship native iOS and Android apps that work with APNs and FCM the right way.
Build Mobile Apps

With both Apple Push Notification service (APNs) and Firebase Cloud Messaging (FCM), delivery is best-effort. The provider will try to deliver your message, but it doesn’t promise the device will show it.

Reachability is the first limiter. You send a notification with a time-to-live (TTL) or expiry. If the device comes back online after that window, the push is dropped. If TTL is very long, the user might see an old alert later, which looks like a bug.

Priority affects timing, but it’s not a free upgrade. High priority can help time-sensitive messages arrive sooner, especially when the device is asleep. Overusing it can lead to throttling, battery drain, or the OS treating your app as noisy.

Both systems support collapsing so a newer message replaces an older one instead of stacking. APNs uses a collapse identifier, and FCM uses a collapse key. If you collapse on something like order_status, the user might only see the latest status, not every step.

Even when the provider delivers successfully, the phone can still prevent the user from seeing it:

  • Do Not Disturb or Focus modes can silence or hide alerts
  • App notification settings can be disabled or set to quiet delivery
  • Android notification channels can be turned off for a specific category
  • Background restrictions or battery savers can delay delivery
  • The OS may suppress repeats if your app posts many similar alerts

Treat push as an unreliable transport: keep important state in your backend, and make the app refresh current status when opened, even if a notification never shows.

Permissions and device settings that affect delivery

Many “delivery issues” are really permission and settings issues.

On iOS, the first permission prompt matters. If the user taps “Don’t Allow,” notifications won’t appear until they change it in Settings. Even after they allow it, they can disable Lock Screen, Notification Center, banners, sounds, or badges. Focus modes and Scheduled Summary can also hide or delay alerts.

On Android, requirements depend on OS version. Newer versions require a runtime notification permission, so an app update can suddenly stop showing notifications until the user approves again. Visibility also depends on notification channels. If the channel is muted or set to low importance, pushes may arrive but never interrupt.

Background restrictions can break expectations too. Low Power Mode on iOS and battery optimizations on Android can delay background work, stop background data, or prevent the app from processing a data-only message.

To confirm what’s happening, log what the device sees, not just what your backend sent:

  • In-app logs: “permission granted,” “token registered,” “notification received,” “notification displayed”
  • OS indicators: notification settings state (enabled/muted/channel importance) and battery mode
  • Push callbacks: whether your app received the message in foreground/background

Even if your backend is built in a no-code tool, client-side logging is still what separates “message not received” from “received but suppressed.”

Step-by-step: how to troubleshoot missing notifications

Build a token-safe backend
Create a backend that upserts tokens, tracks last seen, and avoids duplicate targets.
Start Building

When a push goes missing, treat it like a chain: token, provider, payload, and app behavior. The symptoms can look the same on iOS and Android, so check the same few points in order.

  • Confirm you are sending to a current token. Compare the token stored on your server with the token the app most recently reported. Log when you last received each token.
  • Validate the payload before you send it. Keep it under platform limits, use required fields, and avoid malformed JSON. If you send data-only messages, confirm the app is built to handle them.
  • Check provider credentials and environment. For APNs, confirm key/certificate, team, bundle ID, and whether you’re targeting sandbox vs production. For FCM, confirm the correct project credentials.

Then narrow down whether it’s the message content or the device/app behavior:

  • Send a minimal test notification. A tiny title/body payload helps confirm the transport works.
  • Verify app-side handlers and foreground behavior. Many “missing” pushes are received but not shown. Some apps suppress banners in the foreground by design.
  • Change one variable at a time. Try a second device, a different OS version, Wi-Fi vs cellular, and a different user account. If only one account fails, it often points to stale tokens or server-side targeting.

A practical pattern: if iOS users report misses but Android is fine, start by sending a minimal alert on iOS. If that works, focus on payload structure and app handling. If it doesn’t, focus on tokens and APNs credentials/environment.

Common mistakes that cause silent failures

Add push send audit logs
Keep provider responses and audit trails so “sent” and “displayed” don’t get mixed up.
Set Up Logs

Most push problems aren’t outages. They’re small mismatches between what your app expects and what APNs or FCM will accept, or what the phone will allow.

The most common one is sending to a token that’s no longer valid. Tokens change after reinstall, restore, or refresh. If your server keeps using the old value, pushes go to nowhere.

Another is treating push delivery as guaranteed. Best-effort delivery means late or missing messages are normal when devices are offline or under power-saving rules. For important events (order updates, security alerts), you need an in-app fallback like fetching the latest status on open.

Common causes of missing notifications:

  • Stale iOS push notification tokens or Android tokens kept after reinstall/refresh
  • Exceeding push notification payload limits (too much custom data, oversized images, long strings)
  • Relying on background delivery for silent updates, then getting throttled by OS background limits
  • Mixing iOS environments (development vs production), so token and APNs endpoint don’t match
  • Ignoring user opt-out, Focus/Do Not Disturb, disabled notification channels (Android), or app-level notification permissions

Example: a retail app sends an “order shipped” alert with a large JSON blob of tracking history. The send call looks fine, but the payload is rejected or truncated, and the user sees nothing. Keep the push small and put details behind an API call.

Quick checklist before you blame APNs or FCM

Before you assume the provider is the problem, run a sanity check:

  • Confirm the token is correct for the user and device. It should exist, be updated recently, and be mapped to the right session.
  • Verify provider credentials are valid right now. Check APNs keys/certs and FCM credentials match the right app/project.
  • Validate payload shape and size. Stay under limits and use the correct fields.
  • Set TTL, priority, and collapse on purpose. Low TTL can expire before the phone comes online. Low priority can delay delivery. Collapse can replace earlier messages.
  • Separate “server accepted” from “device displayed.” Compare server logs (request/response/message ID) with client logs (token used, handler called).

Then do a fast device check: notifications allowed for the app, correct channel/category configured (Android channels are a common gotcha), Focus/Do Not Disturb modes, and background restrictions.

Example: diagnosing a missing order update notification

Centralize push token hygiene
Model token storage, logs, and send rules in one place so you can debug faster.
Try AppMaster

A support agent taps “Send order update” for Order #1842. The backend logs show “notification sent,” but the customer never sees anything on their iPhone or Android phone.

Start on the backend. Most “missing” notifications are either never accepted by the push service, or they’re accepted but dropped later because the device can’t (or won’t) show them.

Backend checks first

Look for a single, traceable send attempt (one order update should produce one push request). Then verify:

  • The token used is the most recent token stored for that user and device.
  • The push provider response is a success, and you saved any error code.
  • The payload matches platform rules (size limits, required fields, valid JSON).
  • Auth is valid (APNs key/cert and team/bundle IDs, or FCM credentials).
  • You targeted the right iOS environment (sandbox vs production).

If your logs show a rejection like “unregistered/invalid token,” it’s a token lifecycle issue. If the provider accepts the message but nothing arrives, focus on payload type and OS behavior.

Checks on the phone

Now validate the phone is allowed to show the alert:

  • Notifications are enabled for the app (and allowed for Lock Screen/Banners).
  • Focus/Do Not Disturb or notification summaries aren’t hiding it.
  • Battery saver modes aren’t restricting background work (more common on Android).
  • The app state matches your message type (foreground handling can swallow alerts).

A common outcome: the token is fine, but the message is data-only (Android) or missing the right iOS setup for background handling, so the OS never displays an alert. The fix is to send the right kind of payload for what you want (visible alert vs background update) and to keep clean logs of token updates and provider responses.

Next steps: make push more reliable in your product

Push notifications feel simple until they become a core feature. Reliability comes from the pieces you control: token hygiene, payload discipline, and a fallback path.

Plan for misses. Push is great for “look now” moments, but it shouldn’t be the only route for critical events. An in-app inbox helps users catch up later, and email or SMS can cover high-value actions like password resets or payment issues.

Keep the payload lean. Treat the push payload as a prompt, not the full message. Send an event type and an ID, then fetch details from your backend API when the app opens or when it receives an appropriate background update.

Write a short runbook for your team so debugging stays consistent: opt-in state, token freshness, provider response codes, payload size/shape, and environment/credentials.

If you’re building with AppMaster (appmaster.io), it can be convenient to keep token storage, audit logs, and push-triggering business logic together in one backend, while still shipping native iOS and Android apps that handle APNs and FCM correctly.

FAQ

What’s the simplest way to explain APNs vs FCM?

APNs is Apple’s delivery service for iOS push notifications, and FCM is Google’s delivery service for Android (and can also target iOS via APNs). Your app still decides what to do with the message, but these services determine how you authenticate, how you format payloads, and what delivery behavior you can expect.

Do device tokens stay the same forever?

Treat tokens as changeable addresses. Store them with platform and environment details, update them whenever the app reports a new value, and remove them when the provider tells you they’re invalid. The practical rule is to upsert tokens and keep a “last seen” timestamp so you can spot stale records quickly.

What usually causes an iOS or Android push token to change?

On iOS, tokens commonly change after reinstall, device restore, some OS updates, or when you switch between sandbox and production during development. On Android, FCM tokens can refresh after reinstall, when the user clears app data, when the device is restored, or when Google rotates tokens. Your app should listen for refresh events and immediately send the new token to your backend.

How should I structure a push payload to avoid problems?

Keep the push payload small and treat it like a prompt. Send a short title/body (if you need a visible alert) plus a stable identifier like order_id, then let the app fetch full details from your API. This avoids payload limits, reduces weird edge cases, and makes behavior more consistent across platforms.

What’s the difference between “notification” and “data-only” messages?

A notification payload is meant to be shown to the user, while a data-only payload is meant for the app to process. On Android, data-only messages can be delayed or blocked by background limits and battery settings, so they’re not a reliable trigger for immediate work. On iOS, background pushes are also not guaranteed to run your code right away, so they should be treated as a hint to refresh rather than a real-time job runner.

Are push notifications guaranteed to be delivered and shown?

No, delivery is best-effort. Even if APNs or FCM accepts your request, the device may be offline, the message may expire due to TTL, the OS may throttle delivery, or user settings can suppress alerts. Design your app so critical state is always correct when the user opens it, even if the notification never shows.

What’s the fastest way to debug a missing notification?

Start by separating “sent” from “displayed.” Confirm the token is current, send a minimal title/body test payload, and verify you’re using the correct APNs/FCM credentials and (for iOS) the right environment. If the provider accepts the message, check phone settings like Focus/Do Not Disturb, app notification permissions, and Android notification channels, because the notification can be received but suppressed.

Why do notifications work on Android but not on iOS (or the opposite)?

On iOS, most problems come from permission denial, Focus modes, or targeting the wrong APNs environment (sandbox vs production). On Android, common blockers are runtime notification permission on newer OS versions, muted/low-importance notification channels, and aggressive battery optimization that delays background processing. The same backend send can look fine while the device quietly prevents the user from seeing anything.

How do TTL and “collapse key” affect what users see?

TTL controls how long the provider should keep trying while the device is offline, and collapse settings decide whether newer messages replace older pending ones. A short TTL can make notifications disappear if the phone is offline for a while, and collapse keys/identifiers can make only the latest update show. Set these values intentionally based on the user experience you want, not as defaults you never revisit.

How can AppMaster help me build a reliable push notification setup?

Keep token storage, targeting rules, and send logs together so you can trace each push attempt end-to-end. AppMaster can help by centralizing token tables, audit logging, and push-triggering business logic in one backend, while your native iOS and Android apps still handle APNs and FCM correctly. The key is to log token updates, provider responses, and client-side receipt so you can pinpoint whether the failure is server, provider, or device behavior.

Easy to start
Create something amazing

Experiment with AppMaster with free plan.
When you will be ready you can choose the proper subscription.

Get Started