Push notifications are one of the best ways to re-engage users—when they’re timely, relevant, and reliable. Behind the scenes, Firebase Cloud Messaging (FCM) is the delivery network that gets your payload from your server to the user’s device, whether it’s Android, iOS (through APNs), or the Web (Web Push).
This guide breaks down how FCM works end-to-end—from tokens and topics to priorities, TTL, and delivery behavior—plus production tips and code you can use today.
TL;DR—How a push travels
Core pieces in FCM
- Registration token: a unique, opaque string that identifies an app instance on a device. You get it on the client; your server stores it (user ↔ token mapping).
- FCM backend: Google’s message router. For iOS, FCM relays to APNs; for Web, to the browser’s Push Service.
- Your server: sends messages to FCM using HTTP v1 (recommended) with OAuth 2 (service account).
- Message types:
- Notification payload → OS may display directly (varies by platform/app state).
- Data payload → delivered to your app code; you decide how/when to show.
- Mixed (both): useful, but know each platform’s rules (details below).
The token lifecycle
- App starts → SDK requests a registration token from FCM.
- You send that token to your backend and associate it with the authenticated user.
- Tokens can rotate (reinstall, restore, security, app update). Listen for refresh events and update your backend.
- If you unsubscribe a user (logout), delete the token on the device and in your DB.
Tokens are not secrets but treat them like PII; never commit them to logs you don’t control.
How delivery differs by platform
Android
- Google Play services maintains a persistent connection to FCM.
- If your app is foreground, onMessageReceived (or library hook) fires; you can display a local notification.
- If background and you send a notification payload, the OS can show it automatically.
- Doze/App Standby may delay normal priority data messages. Use high priority sparingly for time-sensitive alerts.
- Use notification channels (Android 8+) to control sound/badges.
iOS (via APNs)
- FCM forwards to APNs using your APNs key.
- Foreground apps don’t auto-show notification banners—use a local notification if desired.
- For silent updates, set content-available: 1 (data-only) and keep payloads small.
- Delivery can be throttled if the app misbehaves or the device is low on power/network.
Web
- Subscribes to browser Push Service with VAPID keys.
- A Service Worker receives the push and shows a notification even if the page is closed.
Message anatomy (HTTP v1)
Key delivery knobs
- Priority: HIGH (time-sensitive) vs NORMAL (default, battery friendly).
- TTL: time to live (seconds). If the device is offline, FCM keeps the message for up to TTL (max 4 weeks).
- android.ttl: "3600s" / apns uses apns-expiration.
- Collapse key / notification key: replace older messages in the same group so users don’t get flooded.
- Topics: broadcast to many devices without managing token lists.
Sending: server code (Node.js / firebase-admin)
Install:
Initialize:
Send to a token:
Send to a topic:
Subscribe tokens to a topic:
Client snippets (Android / iOS / Web)
Android (Kotlin – foreground/refresh)
iOS (Swift – request permission)
Web (Service Worker)
Notification vs Data: which should you use?
- Notification-only
- Pro: OS can show immediately, minimal code.
- Con: Less control (e.g., no custom actions on some states).
- Data-only
- Pro: Full control (decide when/how to display), works the same foreground/background via local notification.
- Con: You must implement the display logic (recommended for consistency).
- Mixed
- Useful for basic banner + extra data. Mind platform differences.
In practice for rich apps, send data-only and render locally (using platform APIs or libraries like Notifee on RN). This gives a consistent UX and analytics.
Priorities, TTL, and collapse
- Use HIGH priority only for truly time-critical events (incoming call, OTP). Overuse can be throttled.
- Set a TTL that matches relevance (e.g., 60–300s for chats, hours for promos).
- Use a collapse key (Android) / apns-collapse-id (iOS) to avoid stacks of stale alerts.
Topics, conditions, and (rare) device groups
- Topics (recommended): subscribe users by interest (e.g., news, promo-in).
- Conditions: boolean expressions of topics:
"'promo' in topics && 'in' in topics".
- Device groups: niche use case (multiple tokens as one “device”); topics usually cover the need.
Security & auth
- Server uses HTTP v1 with OAuth 2: you mint a short-lived access token using your service account; the Admin SDK handles this for you.
- Do not send from the client with your service account—only from your server or cloud functions.
- Never store private keys in the repo; use secrets.
Delivery guarantees & limits
- FCM is best-effort delivery. The OS and device state can delay or drop messages (battery saver, network constraints).
- Keep payloads small (4KB data payload typical limit per platform).
- Don’t spam: use collapse and rate limiting on your side.
Analytics & measuring effectiveness
- Use Firebase Analytics or your own events to measure:
- Delivery (did it arrive? messageId success)
- Open rate (notification taps → deep link)
- Conversion (did they perform the action?)
- Segment by device type, app version, locale, engagement.
Common pitfalls (and fixes)
- Nothing shows in foreground → By design; display a local notification when the app is open.
- iOS never receives → Test on real device, ensure APNs key uploaded, Push + Background modes enabled, signed build.
- Android shows white square icon → Provide proper ic_notification (monochrome).
- “NotRegistered” errors → Token is stale; remove it from your DB.
- High battery usage → Too many high-priority data messages; reduce priority or batch.
Minimal test flow you can run today
- Client: request permission → get token → POST it to your server.
- Server: use firebase-admin to send a message to that token.
- Observe: device logs and server response (you’ll get a messageId).
- Add: deep link handling on notification tap → navigate to a screen.
- Scale: move to topics for broadcasts; store token ↔ user mapping.
Production checklist
- Store & refresh tokens (user ↔ token(s))
- Decide per-event priority and TTL
- Use collapse keys for stream-like updates
- Localize titles/bodies; include deeplink data
- Separate transactional vs marketing channels/topics
- Add metrics: send → delivered → opened → converted
- Implement an unsubscribe/quiet hours strategy
- Guardrails on server to avoid bursts/spam

