AI-powered crash analysis is now available on all plans — including Free.Read the crash analysis guide

Mobile Push Notification Failure Debugging Guide

NFNourin Mahfuj Finick··9 min read

Push notification delivery failures are one of the most insidious problems in mobile development. Unlike a crash that immediately alerts you, a silent push failure means your users simply never see that critical re-engagement message, breaking news alert, or transaction confirmation. For apps that depend on push notifications for user retention, every undelivered notification is lost revenue and a step toward churn. This guide covers how to debug push notification failures across Android's Firebase Cloud Messaging (FCM) and Apple's Push Notification service (APNs), and how to set up proactive monitoring so you catch delivery issues before your users notice.

Why Push Notifications Fail Silently

Push notifications fail for reasons that rarely surface in standard crash reporters. A malformed payload, an expired device token, or a revoked certificate won't throw an exception that shows up in your crash dashboard. Instead, the notification simply never reaches the device, and you won't know unless a user complains — and most never do. According to Airship's benchmark data, opt-in rates for push notifications hover around 50-60% on iOS and 70-80% on Android, but those numbers mean nothing if delivery fails after opt-in. Studies show that push notifications can boost app retention by up to 3-10x, making delivery failures a direct threat to your engagement metrics.

The root causes fall into several categories:

  • Token lifecycle issues: Device tokens expire, get refreshed, or become invalid when users reinstall the app. Sending to a stale token is the most common failure.
  • Payload errors: APNs enforces strict payload size limits (4KB), and FCM rejects malformed JSON. A single oversized field can silently drop a notification.
  • Certificate and authentication failures: iOS push certificates expire annually. FCM server keys can be rotated accidentally. Both cause complete delivery blackouts.
  • Platform-specific restrictions: Android's battery optimization and iOS's notification permission model can block delivery without any error callback.
  • Background processing limits: Notification service extensions on iOS have a ~30-second execution window. If your extension times out while fetching rich media, the notification may not display.

Debugging FCM Delivery Failures on Android

Firebase Cloud Messaging is the standard push delivery mechanism for Android. When messages fail to arrive, start by checking the Firebase Console's Cloud Messaging reports, which show aggregated delivery metrics including send count, received count, and impressions.

Common FCM Error Codes

When sending messages via the FCM HTTP v1 API, the response includes error codes that pinpoint the problem:

  • UNREGISTERED (HTTP 404): The device token is no longer valid. The app instance has been uninstalled, or the token has been refreshed. Always remove these tokens from your server to avoid future failures.
  • INVALID_ARGUMENT (HTTP 400): The message payload is malformed. Check that required fields are present and JSON is well-formed.
  • SENDER_ID_MISMATCH (HTTP 403): The sender ID in the request doesn't match the one associated with the device token. This happens when tokens are shared across Firebase projects incorrectly.
  • QUOTA_EXCEEDED (HTTP 429): You've hit FCM rate limits. Implement exponential backoff in your sending logic.
  • UNAVAILABLE (HTTP 503): FCM servers are temporarily down. Retry with backoff.

Token Refresh Handling

FCM tokens can change at any time. Register an onNewToken callback to capture refreshes:

class BugspulseFirebaseService : FirebaseMessagingService() {
    override fun onNewToken(token: String) {
        super.onNewToken(token)
        // Send the new token to your server
        BugspulsePush.registerToken(token)
        // Log the token refresh event for visibility
        Bugspulse.logEvent("push_token_refreshed", mapOf(
            "token_prefix" to token.take(10)
        ))
    }
}

If you're not capturing token refreshes, you'll accumulate invalid tokens without knowing it. FCM documentation recommends monitoring token generation events and updating your backend accordingly.

Android Notification Channels

On Android 8.0+, notifications won't appear if the channel is disabled. Check channel importance:

val notificationManager = getSystemService(NotificationManager::class.java)
val channel = notificationManager.getNotificationChannel("default")
if (channel?.importance == NotificationManager.IMPORTANCE_NONE) {
    // Channel is blocked — log this as a delivery failure
    Bugspulse.logEvent("push_channel_blocked", mapOf(
        "channel_id" to "default"
    ))
}

Debugging APNs Delivery Failures on iOS

Apple's Push Notification service has its own set of debugging challenges. Unlike FCM, APNs doesn't provide per-message delivery confirmations by default. You need to inspect HTTP/2 response codes from the APNs server.

APNs Error Responses

When using token-based authentication (recommended over certificates), APNs returns status codes for each push attempt:

  • 410 (Unregistered): The device token is inactive for the app. Remove it from your database.
  • 400 (BadRequest): Malformed payload. Check for invalid JSON, missing aps dictionary, or payloads exceeding 4KB.
  • 403 (BadDeviceToken / MissingProviderToken): The device token is invalid for the specified topic, or the auth token is missing.
  • 429 (TooManyRequests): Rate limit exceeded. APNs rate-limits per device token.

Certificate vs. Token Authentication

Apple now recommends token-based authentication (JWT) over certificate-based auth. Token-based keys don't expire and work across all your apps, while push certificates must be renewed annually. A common failure pattern: certificates expire silently, and push delivery stops overnight. Set up certificate expiry monitoring:

// Track APNs auth expiration with a custom event
func checkPushCertificateStatus() {
    let authType = PushConfig.shared.authMethod
    if authType == .certificate {
        let expiryDate = PushConfig.shared.certificateExpiry
        let daysUntilExpiry = Calendar.current.dateComponents(
            [.day], from: Date(), to: expiryDate
        ).day ?? 0
        Bugspulse.logEvent("push_cert_expiry_check", properties: [
            "days_remaining": daysUntilExpiry,
            "auth_method": "certificate"
        ])
    }
}

Notification Service Extensions

For rich notifications with images or modified content, iOS uses a Notification Service Extension with a strict ~30-second window. If your extension crashes or times out, the notification may display without the rich content, or in some cases, not at all. Always wrap extension logic in try-catch blocks and set aggressive timeouts:

class NotificationService: UNNotificationServiceExtension {
    override func didReceive(_ request: UNNotificationRequest, 
                             withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
        let modifiedContent = request.content.mutableCopy() as! UNMutableNotificationContent
        
        // Set a fallback timer
        let fallbackTimer = DispatchWorkItem {
            contentHandler(modifiedContent)
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + 25, execute: fallbackTimer)
        
        // Attempt attachment download
        Task {
            do {
                if let attachment = try await downloadAttachment(from: request.content) {
                    modifiedContent.attachments = [attachment]
                }
            } catch {
                Bugspulse.logError(error, context: "push_attachment_download")
            }
            fallbackTimer.cancel()
            contentHandler(modifiedContent)
        }
    }
}

React Native and Flutter Push Debugging

Cross-platform frameworks add their own complexities to push notification debugging.

React Native

React Native push notification libraries like react-native-push-notification and @notifee/react-native abstract the native layers but can mask underlying failures. Debug by checking both the native and JavaScript layers:

import notifee from '@notifee/react-native';
 
async function checkPushPermission() {
  const settings = await notifee.getNotificationSettings();
  if (settings.authorizationStatus !== notifee.AuthorizationStatus.AUTHORIZED) {
    // Log permission state for monitoring
    logEvent('push_permission_denied', { 
      status: settings.authorizationStatus,
      platform: Platform.OS 
    });
  }
}

For React Native FCM integration, ensure your google-services.json (Android) and GoogleService-Info.plist (iOS) are correctly configured. A frequently overlooked issue: on iOS, you must enable the Push Notifications capability and add the Background Modes capability with "Remote notifications" checked.

Flutter

Flutter's firebase_messaging plugin handles most of the heavy lifting, but token refresh and permission changes still need explicit handling:

FirebaseMessaging.instance.onTokenRefresh.listen((newToken) {
  // Sync token to your backend
  BugspulsePush.syncToken(newToken);
  // Track the refresh event
  FirebaseAnalytics.instance.logEvent(name: 'push_token_refresh');
});

Setting Up Push Notification Health Monitoring

Logging individual failures is a start, but what you need is a dashboard that shows push health at a glance. Instrument your push infrastructure to emit metrics for:

  1. Delivery rate: Sent notifications vs. confirmed deliveries
  2. Failure breakdown: By error code (UNREGISTERED, INVALID_ARGUMENT, etc.)
  3. Token health: Percentage of valid vs. expired tokens in your database
  4. Latency: Time from send to device receipt
  5. Permission opt-out rate: Track when users disable notifications so you can diagnose drop-offs

Here's a practical approach using Bugspulse custom events:

// Track a push delivery end-to-end
fun trackPushDelivery(messageId: String, status: String, errorCode: String? = null) {
    Bugspulse.logEvent("push_delivery", mapOf(
        "message_id" to messageId,
        "status" to status,          // "delivered", "failed", "opened"
        "error_code" to (errorCode ?: ""),
        "platform" to "android",
        "app_version" to BuildConfig.VERSION_NAME
    ))
}

Combine this with your existing logging infrastructure so push failures surface alongside other application errors in a single view. When a certificate expires at 3 AM, you want an alert — not a support ticket from users three days later.

Building a Push Delivery Dashboard

A dedicated push monitoring dashboard should track at minimum these four KPIs:

  1. Delivery success rate — the percentage of sent push notifications that successfully reach the device. Industry benchmarks from OneSignal's 2025 report show average delivery rates of 95% for Android and 88% for iOS, with iOS rates varying significantly by app version adoption.

  2. Notification open rate — delivery is only half the battle. Track what percentage of delivered notifications users actually open. If your open rate drops suddenly, it could indicate that notifications are being delivered but not displayed properly, or that your content timing has fallen out of alignment with user behavior.

  3. Token invalidation rate — the percentage of your active device tokens that FCM or APNs reports as invalid. A sudden spike often correlates with a new app release that accidentally changes the bundle ID, Firebase project configuration, or APNs topic.

  4. Latency p50/p95 — time from server-side send to on-device receipt. APNs typically delivers within 200ms, but FCM on Android can vary from 100ms to several seconds depending on device manufacturer power management and network conditions.

Set up automated alerts for when any of these metrics cross a threshold. For example: "if push delivery rate drops below 90% for more than 5 minutes, alert the on-call engineer." This transforms push from a fire-and-forget channel into a monitored, reliable communication pipeline.

Common Pitfalls and Their Fixes

Problem Symptom Fix
Expired APNs certificate All iOS push fails silently Migrate to token-based (JWT) authentication. Set a calendar reminder to renew certificates if you must use them.
Stale FCM tokens Rising UNREGISTERED errors Implement onNewToken callback and update your token database in real time.
Android battery optimization Notifications arrive late or not at all Request users to exempt your app from battery optimization. Test on Samsung, Xiaomi, and Huawei devices — their aggressive power management is a known source of FCM delivery issues.
Payload > 4KB APNs rejects silently Validate payload size before sending. Strip unnecessary metadata. For rich content, deliver images via URL rather than embedding.
iOS provisional auth Users in "provisional" mode don't see alerts Check authorizationStatus and prompt for full authorization at an appropriate time in your onboarding flow.

Conclusion

Push notification debugging requires a different approach from standard crash monitoring. The failures are silent, the debugging surface spans server-side and client-side, and the platforms diverge significantly in their error reporting. By instrumenting your token lifecycle, actively monitoring delivery rates, and setting up alerts for systemic failures like certificate expirations, you can catch push delivery issues before they impact your engagement metrics.

The most effective strategy is to treat push delivery as a first-class reliability metric, right alongside crash-free rate and API latency. When every undelivered notification is a missed opportunity to re-engage a user, monitoring push health isn't optional — it's essential for maintaining the user experience your app promises.

Ready to monitor your push notification health alongside crashes and performance? Bugspulse gives you a unified view of everything affecting your mobile app's reliability — from FCM errors to APNs certificate expirations. Get started with a free account and ship with confidence.


For more mobile reliability guides, check out our complete mobile app observability guide and learn how to set up comprehensive monitoring at bugspulse.com.