Article

How Firebase Cloud Messaging (FCM) Works: The Complete Guide for Mobile & Web Push

·8 min read min read·👁 60
Dharmendra Singh Yadav

Dharmendra Singh Yadav

Founder, Dharmsy Innovations

How Firebase Cloud Messaging (FCM) Works: The Complete Guide for Mobile & Web Push

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

Your Server (firebase-admin / HTTP v1)
│ (OAuth2 with Service Account)
FCM Backend ──► iOS: hands off to APNs
│ │
├─► Android: Google Play services keeps a
│ persistent socket to FCM on the device
└─► Web: browser Push Service (VAPID) → Service Worker

Device state decides when/how the OS shows your notification.

Core pieces in FCM

  1. 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).
  2. FCM backend: Google’s message router. For iOS, FCM relays to APNs; for Web, to the browser’s Push Service.
  3. Your server: sends messages to FCM using HTTP v1 (recommended) with OAuth 2 (service account).
  4. Message types:
  5. Notification payload → OS may display directly (varies by platform/app state).
  6. Data payload → delivered to your app code; you decide how/when to show.
  7. Mixed (both): useful, but know each platform’s rules (details below).

The token lifecycle

  1. App starts → SDK requests a registration token from FCM.
  2. You send that token to your backend and associate it with the authenticated user.
  3. Tokens can rotate (reinstall, restore, security, app update). Listen for refresh events and update your backend.
  4. 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

  1. Google Play services maintains a persistent connection to FCM.
  2. If your app is foreground, onMessageReceived (or library hook) fires; you can display a local notification.
  3. If background and you send a notification payload, the OS can show it automatically.
  4. Doze/App Standby may delay normal priority data messages. Use high priority sparingly for time-sensitive alerts.
  5. Use notification channels (Android 8+) to control sound/badges.

iOS (via APNs)

  1. FCM forwards to APNs using your APNs key.
  2. Foreground apps don’t auto-show notification banners—use a local notification if desired.
  3. For silent updates, set content-available: 1 (data-only) and keep payloads small.
  4. Delivery can be throttled if the app misbehaves or the device is low on power/network.

Web

  1. Subscribes to browser Push Service with VAPID keys.
  2. A Service Worker receives the push and shows a notification even if the page is closed.

Message anatomy (HTTP v1)

{
"message": {
"token": "<DEVICE_TOKEN>",
"notification": {
"title": "New message",
"body": "You have a new chat"
},
"data": {
"chatId": "abc123",
"type": "message"
},
"android": {
"priority": "HIGH",
"notification": { "channel_id": "chat" }
},
"apns": {
"headers": { "apns-priority": "10" },
"payload": { "aps": { "content-available": 1 } }
},
"webpush": {
"headers": { "Urgency": "high" }
}
}
}

Key delivery knobs

  1. Priority: HIGH (time-sensitive) vs NORMAL (default, battery friendly).
  2. TTL: time to live (seconds). If the device is offline, FCM keeps the message for up to TTL (max 4 weeks).
  3. android.ttl: "3600s" / apns uses apns-expiration.
  4. Collapse key / notification key: replace older messages in the same group so users don’t get flooded.
  5. Topics: broadcast to many devices without managing token lists.

Sending: server code (Node.js / firebase-admin)

Install:

npm i firebase-admin

Initialize:

import admin from "firebase-admin";
import serviceAccount from "./service-account.json" assert { type: "json" };

admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
});

Send to a token:

const res = await admin.messaging().send({
token, // device registration token
notification: { title: "Welcome", body: "Thanks for joining!" },
data: { screen: "welcome" },
android: { priority: "HIGH" },
apns: { payload: { aps: { sound: "default" } } }
});
console.log("FCM messageId:", res);

Send to a topic:

await admin.messaging().send({
topic: "news",
notification: { title: "Breaking", body: "New feature shipped" },
});

Subscribe tokens to a topic:

await admin.messaging().subscribeToTopic(tokensArray, "news");

Client snippets (Android / iOS / Web)

Android (Kotlin – foreground/refresh)

class MyFirebaseService : FirebaseMessagingService() {

override fun onNewToken(token: String) {
super.onNewToken(token)
// POST token to your backend
}

override fun onMessageReceived(msg: RemoteMessage) {
// If you included data payload, handle it here
val title = msg.notification?.title ?: "Update"
val body = msg.notification?.body ?: ""
// Build a notification with NotificationCompat + channel "chat"
}
}

iOS (Swift – request permission)

UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, _ in
DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() }
}

Web (Service Worker)

// firebase-messaging-sw.js
importScripts('https://www.gstatic.com/firebasejs/10.12.0/firebase-app-compat.js');
importScripts('https://www.gstatic.com/firebasejs/10.12.0/firebase-messaging-compat.js');

firebase.initializeApp({ /* your config */ });
const messaging = firebase.messaging();

messaging.onBackgroundMessage(({ notification }) => {
self.registration.showNotification(notification.title, {
body: notification.body,
icon: "/icon.png",
});
});

Notification vs Data: which should you use?

  1. Notification-only
  2. Pro: OS can show immediately, minimal code.
  3. Con: Less control (e.g., no custom actions on some states).
  4. Data-only
  5. Pro: Full control (decide when/how to display), works the same foreground/background via local notification.
  6. Con: You must implement the display logic (recommended for consistency).
  7. Mixed
  8. 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

  1. Use HIGH priority only for truly time-critical events (incoming call, OTP). Overuse can be throttled.
  2. Set a TTL that matches relevance (e.g., 60–300s for chats, hours for promos).
  3. Use a collapse key (Android) / apns-collapse-id (iOS) to avoid stacks of stale alerts.

Topics, conditions, and (rare) device groups

  1. Topics (recommended): subscribe users by interest (e.g., news, promo-in).
  2. Conditions: boolean expressions of topics:

"'promo' in topics && 'in' in topics".

  1. Device groups: niche use case (multiple tokens as one “device”); topics usually cover the need.

Security & auth

  1. 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.
  2. Do not send from the client with your service account—only from your server or cloud functions.
  3. Never store private keys in the repo; use secrets.

Delivery guarantees & limits

  1. FCM is best-effort delivery. The OS and device state can delay or drop messages (battery saver, network constraints).
  2. Keep payloads small (4KB data payload typical limit per platform).
  3. Don’t spam: use collapse and rate limiting on your side.

Analytics & measuring effectiveness

  1. Use Firebase Analytics or your own events to measure:
  2. Delivery (did it arrive? messageId success)
  3. Open rate (notification taps → deep link)
  4. Conversion (did they perform the action?)
  5. Segment by device type, app version, locale, engagement.

Common pitfalls (and fixes)

  1. Nothing shows in foreground → By design; display a local notification when the app is open.
  2. iOS never receives → Test on real device, ensure APNs key uploaded, Push + Background modes enabled, signed build.
  3. Android shows white square icon → Provide proper ic_notification (monochrome).
  4. “NotRegistered” errors → Token is stale; remove it from your DB.
  5. High battery usage → Too many high-priority data messages; reduce priority or batch.

Minimal test flow you can run today

  1. Client: request permission → get token → POST it to your server.
  2. Server: use firebase-admin to send a message to that token.
  3. Observe: device logs and server response (you’ll get a messageId).
  4. Add: deep link handling on notification tap → navigate to a screen.
  5. Scale: move to topics for broadcasts; store token ↔ user mapping.

Production checklist

  1. Store & refresh tokens (user ↔ token(s))
  2. Decide per-event priority and TTL
  3. Use collapse keys for stream-like updates
  4. Localize titles/bodies; include deeplink data
  5. Separate transactional vs marketing channels/topics
  6. Add metrics: send → delivered → opened → converted
  7. Implement an unsubscribe/quiet hours strategy
  8. Guardrails on server to avoid bursts/spam

Frequently Asked Questions

What is 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.

What is 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.

What is 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.

What is 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.

What is 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.

How can Dharmsy help?+

Dharmsy builds production-grade web, mobile, and SaaS products. Share your requirements and we'll give you a clear, honest plan.

Work with Dharmsy Innovations

Turn Your SaaS or App Idea Into a Real Product — Faster & Affordable

Dharmsy Innovations helps founders and businesses turn ideas into production-ready products — from MVP and prototypes to scalable platforms in web, mobile, and AI.

No sales pressure — just honest guidance on cost, timeline & tech stack.