Loading content...
Every mobile push notification you've ever received on iOS or Android passed through one of two gatekeepers: Apple Push Notification service (APNs) for iOS, iPadOS, macOS, watchOS, and tvOS devices, or Firebase Cloud Messaging (FCM) for Android devices and web browsers. These platforms are not optional middleware—they are the only officially supported channels for push notification delivery on their respective platforms.
Understanding the intricacies of APNs and FCM is essential for building reliable push notification systems. While both services serve the same fundamental purpose—delivering messages from servers to devices—they differ significantly in their APIs, authentication methods, error handling, and behavioral characteristics.
This page provides a comprehensive deep-dive into both platforms, examining their architectures, APIs, best practices, and the subtle differences that impact delivery reliability and performance.
Both APNs and FCM have evolved significantly over the years. APNs transitioned from a binary protocol to HTTP/2, and FCM emerged from a consolidation of Google Cloud Messaging (GCM) and other Google messaging services. The content in this page reflects current best practices as of 2024-2025, but always consult official documentation for the latest updates.
Apple Push Notification service operates as a globally distributed system that maintains persistent connections with billions of Apple devices. Understanding its architecture helps explain its behavior and informs optimal integration patterns.
APNs Infrastructure Overview:
APNs operates across Apple's worldwide data center infrastructure with endpoints in multiple geographic regions. The service handles:
| Environment | Endpoint | Port | Use Case |
|---|---|---|---|
| Production | api.push.apple.com | 443 | Production app notifications |
| Development | api.sandbox.push.apple.com | 443 | Development/testing |
HTTP/2 Protocol Advantages:
APNs uses HTTP/2 exclusively, which provides significant advantages over the legacy binary protocol:
Multiplexing: Send multiple notification requests concurrently over a single connection without head-of-line blocking. This dramatically improves throughput.
Header Compression: HPACK compression reduces overhead for repeated headers, important when sending many notifications rapidly.
Immediate Feedback: Each request receives an immediate response indicating success or failure, unlike the legacy protocol that provided asynchronous feedback.
Standard Tooling: HTTP/2 integrates with standard HTTP tools, making debugging and monitoring easier.
Request Structure:
APNs requests use standard HTTP/2 request semantics with Apple-specific headers:
123456789101112131415161718192021222324
# APNs HTTP/2 Request Structure :method = POST:path = /3/device/{device_token}:scheme = httpsauthorization = bearer {jwt_token} # Token-based authapns-topic = com.yourcompany.app # Bundle IDapns-push-type = alert # alert, background, voip, complication, fileprovider, mdmapns-priority = 10 # 10 (immediate) or 5 (throttleable)apns-expiration = 0 # 0 = immediate delivery only, or Unix timestampapns-collapse-id = collapse-123 # Optional: replaces previous notification with same ID # Request Body (JSON payload){ "aps": { "alert": { "title": "Message Title", "body": "Message body text" }, "sound": "default", "badge": 1 }, "customData": "your-custom-payload"}The apns-push-type header is mandatory in iOS 13+ and must accurately reflect your payload. Setting it incorrectly can cause delivery failures or app rejections. Use 'alert' for visible notifications, 'background' for content-available (silent) notifications, and 'voip' for VoIP call notifications (which require a VoIP entitlement).
APNs authentication establishes your server's identity and authorization to send notifications. Apple provides two authentication mechanisms, each with distinct characteristics:
Token-Based Authentication (JWT) — Recommended:
Token-based authentication uses JSON Web Tokens signed with a private key that you generate in the Apple Developer portal. This approach is preferred for several reasons:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
import jwtimport timefrom pathlib import Path class APNsAuthenticator: """ Manages JWT generation for APNs token-based authentication. Key components: - Key ID: 10-character identifier from Apple Developer Portal - Team ID: 10-character Apple Developer Team identifier - Private Key: The .p8 file contents (ES256 private key) """ ALGORITHM = "ES256" TOKEN_LIFETIME_SECONDS = 3600 # 1 hour maximum def __init__( self, key_id: str, team_id: str, private_key_path: str ): self.key_id = key_id self.team_id = team_id self.private_key = Path(private_key_path).read_text() # Token caching self._cached_token: str | None = None self._token_generation_time: float = 0 def get_authorization_header(self) -> str: """ Returns the Bearer token for APNs authentication. Caches token and refreshes before expiration. """ current_time = time.time() # Refresh if token doesn't exist or is within 5 minutes of expiry if ( not self._cached_token or current_time - self._token_generation_time > self.TOKEN_LIFETIME_SECONDS - 300 ): self._cached_token = self._generate_token() self._token_generation_time = current_time return f"bearer {self._cached_token}" def _generate_token(self) -> str: """ Generates a new JWT for APNs authentication. Token structure (RFC 7519): - Header: algorithm and key ID - Payload: issuer (team ID) and issued-at timestamp - Signature: ES256 signature using private key """ headers = { "alg": self.ALGORITHM, "kid": self.key_id, } payload = { "iss": self.team_id, "iat": int(time.time()), } token = jwt.encode( payload, self.private_key, algorithm=self.ALGORITHM, headers=headers ) return token def force_refresh(self) -> None: """ Forces token regeneration on next request. Useful after receiving 403 InvalidProviderToken errors. """ self._cached_token = None self._token_generation_time = 0 # Production usageauthenticator = APNsAuthenticator( key_id="ABC123DEFG", team_id="TEAM123456", private_key_path="/secure/keys/AuthKey_ABC123DEFG.p8") # Get header for each requestauth_header = authenticator.get_authorization_header()Certificate-Based Authentication (Legacy):
Certificate-based authentication uses TLS client certificates. While still supported, this approach has significant operational overhead:
Choosing Between Authentication Methods:
| Aspect | Token-Based (JWT) | Certificate-Based |
|---|---|---|
| Key Scope | Single key for all team apps | Per-app certificate |
| Expiration | Key never expires | 12-month certificate expiry |
| Renewal Process | None required | Annual manual renewal |
| Server Setup | Simple (just store private key) | Complex (install certificate chain) |
| Token Lifetime | 1 hour (auto-refreshed) | N/A |
| Recommended | Yes | Only for legacy systems |
Do not generate a new JWT for every notification request. JWT generation involves cryptographic operations and should be minimized. Cache the token and refresh it proactively (e.g., 5-10 minutes before the 1-hour expiration). APNs may rate-limit excessive token generations.
APNs provides detailed error responses that guide how your system should react. Understanding and properly handling these errors is crucial for maintaining high delivery rates and system health.
Response Status Codes:
| Code | Meaning | Action Required |
|---|---|---|
| 200 | Success | Notification accepted for delivery |
| 400 | Bad Request | Fix request format (check payload, headers) |
| 403 | Forbidden | Authentication issue (refresh token, check certificate) |
| 404 | Not Found | Invalid device token (remove from database) |
| 405 | Method Not Allowed | Only POST is supported |
| 410 | Gone | Device token is no longer valid (remove from database) |
| 413 | Payload Too Large | Reduce payload size (max 4KB) |
| 429 | Too Many Requests | Rate limited, implement backoff |
| 500 | Internal Server Error | APNs issue, retry with backoff |
| 503 | Service Unavailable | APNs overloaded, retry with backoff |
Error Reason Codes:
For non-200 responses, APNs includes a JSON body with a reason field providing specific error details:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
# Comprehensive APNs error handling implementation class APNsErrorHandler: """ Handles APNs error responses with appropriate recovery actions. Error categories: 1. Token-related: Invalidate and stop sending 2. Payload-related: Fix and retry 3. Transient: Retry with backoff 4. Authentication: Refresh credentials """ # Errors that indicate permanent token invalidity TOKEN_INVALID_REASONS = { 'BadDeviceToken', # Token format is invalid 'Unregistered', # App was uninstalled 'DeviceTokenNotForTopic', # Token doesn't match the request topic 'ExpiredToken', # Token has expired (rare) } # Errors that require payload fixes PAYLOAD_ERRORS = { 'PayloadEmpty', # Payload is empty 'PayloadTooLarge', # Exceeds 4KB limit 'BadPayload', # Invalid JSON 'BadMessageId', # Invalid apns-id header 'BadCollapseId', # Invalid collapse ID } # Errors that should trigger retry TRANSIENT_ERRORS = { 'TooManyRequests', # Rate limited 'InternalServerError', # APNs internal error 'ServiceUnavailable', # APNs temporarily unavailable 'Shutdown', # Server is shutting down } # Authentication errors AUTH_ERRORS = { 'InvalidProviderToken', # JWT is invalid or expired 'ExpiredProviderToken', # JWT expired 'MissingProviderToken', # No JWT provided 'BadCertificate', # Certificate issue 'BadCertificateEnvironment', # Cert doesn't match environment 'Forbidden', # Not authorized for this topic } def __init__(self, token_service, metrics, auth_manager): self.token_service = token_service self.metrics = metrics self.auth = auth_manager async def handle_error( self, device_token: str, status_code: int, reason: str | None, response_body: dict | None ) -> ErrorHandleResult: """ Processes APNs error and returns appropriate action. Returns: ErrorHandleResult with: - should_retry: Whether to retry this notification - retry_after: Seconds to wait before retry (if applicable) - should_invalidate_token: Whether to remove this token - action: Human-readable description of action taken """ self.metrics.increment( 'apns.error', tags={'status': status_code, 'reason': reason} ) # Handle by category if reason in self.TOKEN_INVALID_REASONS: return await self._handle_invalid_token(device_token, reason) if reason in self.PAYLOAD_ERRORS: return self._handle_payload_error(reason) if reason in self.AUTH_ERRORS: return self._handle_auth_error(reason) if reason in self.TRANSIENT_ERRORS or status_code >= 500: return self._handle_transient_error(reason, response_body) # Unknown error - log and don't retry logger.error(f"Unknown APNs error: {status_code} - {reason}") return ErrorHandleResult( should_retry=False, should_invalidate_token=False, action=f"Unknown error: {reason}" ) async def _handle_invalid_token( self, device_token: str, reason: str ) -> ErrorHandleResult: """ Handle permanently invalid tokens. These should be removed from the database immediately. """ await self.token_service.invalidate_token( token=device_token, reason=reason ) return ErrorHandleResult( should_retry=False, should_invalidate_token=True, action=f"Token invalidated: {reason}" ) def _handle_transient_error( self, reason: str, response_body: dict | None ) -> ErrorHandleResult: """ Handle transient errors with retry. Uses exponential backoff with jitter. Respects Retry-After header if present. """ if reason == 'TooManyRequests': # Rate limited - check for Retry-After hint retry_after = 60 # Default 1 minute return ErrorHandleResult( should_retry=True, retry_after=retry_after, should_invalidate_token=False, action="Rate limited, will retry" ) # Server errors - retry with backoff return ErrorHandleResult( should_retry=True, retry_after=5, # Start with 5 seconds should_invalidate_token=False, action=f"Transient error: {reason}, will retry" ) def _handle_auth_error(self, reason: str) -> ErrorHandleResult: """ Handle authentication errors. Typically requires refreshing the JWT token. """ if reason in ('InvalidProviderToken', 'ExpiredProviderToken'): self.auth.force_refresh() return ErrorHandleResult( should_retry=True, retry_after=1, should_invalidate_token=False, action="Token refreshed, will retry" ) # Other auth errors may require configuration fixes return ErrorHandleResult( should_retry=False, should_invalidate_token=False, action=f"Auth error requires manual intervention: {reason}" )The 410 Gone response with reason 'Unregistered' indicates the user has uninstalled your app. You MUST remove this token from your database. Continuing to send to unregistered tokens wastes resources and may impact your reputation with APNs, potentially leading to increased delivery delays.
Firebase Cloud Messaging (FCM) is Google's push notification service, handling delivery to Android devices, web browsers (via web push), and iOS devices (though most iOS apps use APNs directly). FCM originated from Google Cloud Messaging (GCM) and has become the unified messaging platform for Firebase.
FCM Infrastructure Components:
Firebase Backend: Handles authentication, message routing, and device registration.
Connection Servers: Maintain persistent connections with Android devices (via Google Play Services).
HTTP v1 API: The modern REST API for sending messages from your server.
XMPP Interface: Legacy bidirectional connection for real-time use cases (being deprecated).
FCM Message Flow:
Unlike APNs where your server communicates directly with Apple's push servers, FCM provides an additional abstraction layer:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
// FCM HTTP v1 API Request StructurePOST https://fcm.googleapis.com/v1/projects/{project_id}/messages:sendAuthorization: Bearer {oauth2_access_token}Content-Type: application/json { "message": { "token": "device_registration_token", // Notification payload (displayed by system) "notification": { "title": "Message Title", "body": "Message body text", "image": "https://example.com/notification-image.png" }, // Data payload (passed to app) "data": { "conversation_id": "abc123", "message_type": "chat" }, // Android-specific configuration "android": { "priority": "high", "ttl": "86400s", "collapse_key": "new_messages", "notification": { "icon": "notification_icon", "color": "#4285F4", "channel_id": "messages", "click_action": "OPEN_CHAT" } }, // Web push configuration "webpush": { "headers": { "Urgency": "high" }, "notification": { "icon": "https://example.com/icon.png" } } }}Key FCM Concepts:
Registration Tokens: Each app instance receives a unique registration token upon installation. Unlike APNs device tokens (which are tied to the app-device combination), FCM registration tokens are tied to the specific app installation and can change for various reasons.
Topic Messaging: FCM provides built-in topic support, allowing you to send a single message that's delivered to all devices subscribed to a topic. This is handled entirely by FCM—you don't need to maintain the subscription list.
Message Types:
FCM distinguishes between two message types with significantly different behaviors:
| Aspect | Notification Messages | Data Messages |
|---|---|---|
| Display | Displayed by system automatically | App handles display |
| App in Foreground | Delivered to app | Delivered to app |
| App in Background | Displayed by system, tap opens app | Delivered to app (if background service exists) |
| App Terminated | Displayed by system | May be delayed until app launch |
| Max Size | 4KB | 4KB |
| Use Case | Simple display notifications | Custom handling, silent updates |
For maximum control over notification display (custom layouts, sounds, grouping), prefer data-only messages. Your app receives the data and constructs the notification UI. This requires implementing a Firebase Messaging Service that runs in the background, but gives you complete control over the user experience.
FCM uses Google's standard OAuth 2.0 authentication via service accounts. This is the only supported authentication method for the HTTP v1 API (the legacy API with server keys is deprecated).
Service Account Setup:
Access Token Generation:
The service account key is used to generate short-lived access tokens (typically 1 hour). Google's client libraries handle this automatically:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
from google.oauth2 import service_accountfrom google.auth.transport.requests import Requestimport google.auth class FCMAuthenticator: """ Manages OAuth 2.0 access token generation for FCM. Uses Google's official auth libraries which handle: - Token generation from service account - Automatic token refresh before expiration - Caching to minimize token generation calls """ # FCM requires this specific scope FCM_SCOPE = 'https://www.googleapis.com/auth/firebase.messaging' def __init__(self, service_account_path: str): """ Initialize with path to service account JSON key file. The key file contains: - project_id: Your Firebase project ID - private_key: RSA private key for signing - client_email: Service account email """ self.credentials = service_account.Credentials.from_service_account_file( service_account_path, scopes=[self.FCM_SCOPE] ) self._project_id = self._extract_project_id(service_account_path) def _extract_project_id(self, path: str) -> str: """Extract project ID from service account file.""" import json with open(path) as f: return json.load(f)['project_id'] @property def project_id(self) -> str: """Firebase project ID for constructing FCM endpoint.""" return self._project_id def get_access_token(self) -> str: """ Returns a valid access token, refreshing if necessary. Google's credentials object handles token lifecycle: - First call generates token - Subsequent calls return cached token - Automatically refreshes when expired """ if not self.credentials.valid: self.credentials.refresh(Request()) return self.credentials.token def get_authorization_header(self) -> str: """Returns the Bearer token header value.""" return f"Bearer {self.get_access_token()}" def get_endpoint(self) -> str: """Returns the FCM HTTP v1 API endpoint.""" return f"https://fcm.googleapis.com/v1/projects/{self.project_id}/messages:send" # Production usageauth = FCMAuthenticator('/secure/credentials/firebase-service-account.json') # Make FCM requestimport httpx async def send_fcm_message(message: dict) -> dict: async with httpx.AsyncClient() as client: response = await client.post( auth.get_endpoint(), headers={ 'Authorization': auth.get_authorization_header(), 'Content-Type': 'application/json' }, json={'message': message} ) return response.json()The service account JSON file contains a private key that provides complete access to send notifications to all your app users. Treat it like a password: never commit to version control, never expose in client code, and use secrets management (Vault, AWS Secrets Manager, GCP Secret Manager) in production.
FCM returns structured error responses in the HTTP v1 API. Proper error handling ensures high delivery rates and efficient resource usage.
Response Structure:
Successful requests return HTTP 200 with a message name:
{
"name": "projects/myproject/messages/1234567890"
}
Error responses include detailed information:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
# FCM error handling implementation class FCMErrorHandler: """ Processes FCM error responses with appropriate recovery actions. FCM error codes follow Google's error model with: - HTTP status code (4xx, 5xx) - Error code in response body - Detailed error message """ # Errors indicating invalid registration token TOKEN_INVALID_CODES = { 'UNREGISTERED', # App uninstalled or token invalid 'INVALID_ARGUMENT', # Token format is invalid (when targeting token) } # Transient errors that should be retried TRANSIENT_CODES = { 'UNAVAILABLE', # FCM service temporarily unavailable 'INTERNAL', # FCM internal error 'QUOTA_EXCEEDED', # Rate limit or quota exceeded } # Errors requiring configuration changes CONFIG_ERRORS = { 'SENDER_ID_MISMATCH', # Token was generated with different sender 'THIRD_PARTY_AUTH_ERROR', # APNs auth error (when sending to iOS via FCM) } def __init__(self, token_service, metrics): self.token_service = token_service self.metrics = metrics async def handle_error( self, token: str, status_code: int, error_response: dict ) -> ErrorHandleResult: """ Process FCM error and determine appropriate action. Args: token: The registration token that failed status_code: HTTP status code error_response: Parsed JSON error response Returns: ErrorHandleResult with retry guidance """ # Extract error code from response error_code = self._extract_error_code(error_response) self.metrics.increment( 'fcm.error', tags={'status': status_code, 'code': error_code} ) if error_code in self.TOKEN_INVALID_CODES: return await self._handle_invalid_token(token, error_code) if error_code in self.TRANSIENT_CODES: return self._handle_transient_error(error_code, error_response) if error_code in self.CONFIG_ERRORS: return self._handle_config_error(error_code) # Handle by HTTP status code if status_code == 400: return ErrorHandleResult( should_retry=False, action=f"Bad request: {error_response}" ) if status_code == 401: return ErrorHandleResult( should_retry=True, retry_after=0, # Retry immediately after refreshing auth action="Auth error - refreshing credentials" ) if status_code >= 500: return self._handle_transient_error('SERVER_ERROR', error_response) return ErrorHandleResult( should_retry=False, action=f"Unhandled error: {status_code} - {error_code}" ) def _extract_error_code(self, error_response: dict) -> str: """ Extracts error code from FCM error response. FCM error format: { "error": { "code": 400, "message": "...", "status": "INVALID_ARGUMENT", "details": [ { "@type": "type.googleapis.com/google.firebase.fcm.v1.FcmError", "errorCode": "UNREGISTERED" } ] } } """ try: error = error_response.get('error', {}) # Check details for FCM-specific error code for detail in error.get('details', []): if 'errorCode' in detail: return detail['errorCode'] # Fall back to status return error.get('status', 'UNKNOWN') except (KeyError, TypeError): return 'UNKNOWN' async def _handle_invalid_token( self, token: str, error_code: str ) -> ErrorHandleResult: """ Handle permanently invalid tokens. """ await self.token_service.invalidate_token( token=token, reason=error_code ) return ErrorHandleResult( should_retry=False, should_invalidate_token=True, action=f"Token invalidated: {error_code}" ) def _handle_transient_error( self, error_code: str, error_response: dict ) -> ErrorHandleResult: """ Handle transient errors with retry. FCM may include Retry-After header for rate limiting. """ retry_after = 60 # Default if error_code == 'QUOTA_EXCEEDED': # Back off more aggressively for quota errors retry_after = 300 return ErrorHandleResult( should_retry=True, retry_after=retry_after, action=f"Transient error: {error_code}, retry in {retry_after}s" )The UNREGISTERED error indicates the app was uninstalled or the token is otherwise invalid. Like APNs 410 Gone, you must remove these tokens from your database. FCM may delay or reduce priority for senders who consistently send to invalid tokens.
While both APNs and FCM serve the same fundamental purpose, their operational characteristics differ in ways that impact your architecture decisions:
Delivery Guarantees:
Neither platform provides guaranteed delivery. Both use a 'best effort' model where notifications may be dropped if devices are unreachable for extended periods:
APNs: Stores only the most recent notification per app for offline devices. If you send multiple notifications while a device is offline, only the last one is delivered.
FCM: Stores up to 100 pending messages per device, respecting TTL settings. Collapsible messages (with collapse_key) replace earlier messages with the same key.
| Aspect | APNs (iOS) | FCM (Android) |
|---|---|---|
| Protocol | HTTP/2 over TLS | HTTPS (HTTP/2 supported) |
| Authentication | JWT (recommended) or TLS Certificate | OAuth 2.0 Service Account |
| Max Payload | 4KB | 4KB |
| Offline Storage | 1 notification per app | Up to 100 messages per device |
| Max TTL | 30 days | 28 days |
| Topic Messaging | Not built-in (you manage) | Native support |
| Device Groups | Not built-in | Native support |
| Message Priority | 5 (normal) or 10 (high) | normal or high |
| Delivery Receipt | Not available | Available via Analytics |
| Rate Limits | Undisclosed, generous | Documented, per-device limits |
Building a Unified Notification Layer:
For applications targeting both platforms, build an abstraction layer that:
Normalizes Payload Construction: Accept a platform-agnostic notification object and translate to APNs/FCM-specific formats.
Routes by Platform: Lookup device tokens and route based on stored platform identifier.
Unified Error Handling: Map platform-specific errors to common categories (invalid_token, transient, auth_error, etc.).
Common Metrics: Track delivery success, error rates, and latency using platform-agnostic metrics, with platform tags for drill-down.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
# Unified notification layer abstraction from abc import ABC, abstractmethodfrom dataclasses import dataclassfrom enum import Enum class Platform(Enum): IOS = "ios" ANDROID = "android" @dataclassclass UnifiedNotification: """Platform-agnostic notification structure.""" title: str body: str badge: int | None = None sound: str | None = "default" data: dict | None = None image_url: str | None = None priority: str = "normal" # "normal" or "high" ttl_seconds: int = 86400 # Default 24 hours collapse_key: str | None = None class PushProvider(ABC): """Abstract base for platform-specific providers.""" @abstractmethod async def send( self, token: str, notification: UnifiedNotification ) -> SendResult: pass class UnifiedPushService: """ Unified push notification service that abstracts platform differences. """ def __init__( self, apns_provider: APNsProvider, fcm_provider: FCMProvider, token_registry: TokenRegistry ): self.providers = { Platform.IOS: apns_provider, Platform.ANDROID: fcm_provider, } self.tokens = token_registry async def send_to_user( self, user_id: str, notification: UnifiedNotification ) -> list[SendResult]: """ Send notification to all devices registered to a user. Returns results for each device attempt. """ tokens = await self.tokens.get_user_tokens(user_id) results = [] for token_info in tokens: provider = self.providers[token_info.platform] result = await provider.send(token_info.token, notification) results.append(result) return results async def send_to_segment( self, segment_query: dict, notification: UnifiedNotification ) -> BatchSendResult: """ Send notification to all users matching segment criteria. Uses streaming/pagination to handle large segments efficiently. """ total_sent = 0 total_failed = 0 async for token_batch in self.tokens.stream_by_segment(segment_query): # Group by platform for batched sending ios_tokens = [t for t in token_batch if t.platform == Platform.IOS] android_tokens = [t for t in token_batch if t.platform == Platform.ANDROID] # Send to each platform for token in ios_tokens: result = await self.providers[Platform.IOS].send( token.token, notification ) if result.success: total_sent += 1 else: total_failed += 1 for token in android_tokens: result = await self.providers[Platform.ANDROID].send( token.token, notification ) if result.success: total_sent += 1 else: total_failed += 1 return BatchSendResult( total_sent=total_sent, total_failed=total_failed )We've thoroughly examined the two dominant mobile push platforms. Here are the essential takeaways:
What's Next:
With mobile platforms covered, we'll next explore web push notifications. You'll learn how the Web Push Protocol enables push notifications to browsers, the role of service workers, and how web push differs from mobile push in capabilities and constraints.
You now have a comprehensive understanding of APNs and FCM—their architectures, authentication mechanisms, payload structures, and error handling patterns. This knowledge is essential for building reliable push notification systems that deliver notifications efficiently to both iOS and Android users.