Keeping FCM tokens current
When an FCM token changes, how to send it to your server on every start, when to delete it (UNREGISTERED, 270 days, one month stale), and the move to Firebase Installation IDs.
An FCM token is not permanent. It changes when the app moves to a new phone or its data is cleared, and it stops working when a device has been gone for long enough. If your server keeps the old one, pushes fail quietly and your delivery numbers fall for no visible reason. This page covers when tokens change, how to keep the server’s copy current, and Firebase’s move from registration tokens to Firebase Installation IDs (FIDs).
Tokens are becoming installation IDs
Firebase’s current docs mark registration tokens as deprecated. The replacement is the Firebase Installation ID, and the docs say both are “fully co-supported” during the transition:
| Registration token (deprecated) | Firebase Installation ID | |
|---|---|---|
| Android callback | onNewToken(token) |
onRegistered(installationId) |
| Android opt-in | none | firebase_messaging_installation_id_enabled meta-data set to true |
| Apple callback | messaging(_:didReceiveRegistrationToken:) |
messaging(_:didReceiveRegistration:) |
| Apple opt-in | none | FirebaseMessagingInstallationIdEnabled = YES in Info.plist |
| HTTP v1 field | token |
fid (the token field accepts an FID during the transition) |
| Refresh | your own monthly job | the SDK calls the callback on routine syncs at app start |
The server side is the same either way: store one identifier per app install with the time it was last uploaded, and delete it when FCM says it is gone. The rest of this page says “token” for both.
When the token changes
Firebase lists three cases:
- The app is restored on a new device.
- The user uninstalls and reinstalls the app.
- The user clears the app’s data.
Each gives a new token through the callback. The old one stops working, and your next send to it returns UNREGISTERED.
Separately, a token can go stale without changing. Firebase calls a registration stale when the device has not connected to FCM for over a month. On Android, after 270 days without a connection FCM treats it as expired, deletes it and rejects sends to it. Apple devices have no such rule, because FCM relies on APNs there.
Send it to the server on every start
On Android, with installation IDs:
class PushService : FirebaseMessagingService() {
// Called on routine syncs at app start and whenever the ID changes.
override fun onRegistered(installationId: String) {
sendRegistrationToServer(installationId)
}
}
With the deprecated token API, onNewToken fires when a token is first made and when it changes, but not on every start. Fetch the current one at startup too, so a failed upload is retried:
override fun onNewToken(token: String) {
sendRegistrationToServer(token)
}
// In your Application or main activity:
FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
if (task.isSuccessful) sendRegistrationToServer(task.result)
}
On Apple platforms, set Messaging.messaging().delegate after FirebaseApp.configure() and upload from the delegate method. The token only arrives once the APNs device token has been mapped, so call registerForRemoteNotifications() on every launch.
On the web, getToken(messaging, { vapidKey }) returns the current token, from cache after the first call. Call it on each visit where permission is granted and upload the result.
What the server keeps
One row per app install: the user, the platform, the token and a last_seen time. Firebase “strongly” recommends the timestamp, updated on every upload even when the token has not changed.
CREATE TABLE device (
token TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
platform TEXT NOT NULL,
last_seen INTEGER NOT NULL
);
-- On every upload: insert, or move the token to this user and touch the time.
INSERT INTO device (token, user_id, platform, last_seen) VALUES (?, ?, ?, ?)
ON CONFLICT (token) DO UPDATE SET user_id = excluded.user_id, last_seen = excluded.last_seen;
Key the row on the token, not the user: one person has several phones, and one phone can change hands.
Delete dead tokens
Two kinds of removal:
- On a send error. With HTTP v1,
UNREGISTERED(404) means the token will never work again.INVALID_ARGUMENT(400) means the same only if the payload is valid, because it is also the answer to a malformed message. Firebase: “it is safe to delete your record of this registration”. See FCM error codes for the rest. - By age. Firebase’s default staleness window is one month, and it suggests a daily job that deletes rows older than it:
const MONTH = 1000 * 60 * 60 * 24 * 30;
await db.prepare("DELETE FROM device WHERE last_seen < ?").bind(Date.now() - MONTH).run();
Pick the window for your app: one month may be too short for something people open quarterly. A device that comes back after pruning uploads its token again at the next start, so nothing is lost for good.
How often to refresh
With installation IDs and auto-initialization, the SDK keeps the registration fresh and calls onRegistered on routine syncs; Firebase says you do not need a scheduled job in the app. With the deprecated tokens, Firebase recommends fetching and uploading the token periodically, for example with WorkManager. Its advice: about once a month “strikes a good balance between battery impact and detecting inactive registration tokens”, and there is “no benefit” to refreshing more often than weekly.
Topics
If you use topic messaging, subscribe again whenever the token changes, since the subscription belongs to the old token. Firebase also suggests unsubscribing installs that have been idle for your staleness window, so fan-outs are faster and delivery numbers are honest.
With SignOps
SignOps Push keeps the device table for you. Your server adds each token with POST /v1/devices:
curl https://api.signops.dev/v1/devices \
-H "Authorization: Bearer $SIGNOPS_KEY" \
-d user_id=usr_8f21c4 -d platform=android -d token=$FCM_TOKEN
Adding the same token again updates its last-seen time, moves it to the given user if it changed hands, and brings it back if it had been removed. When FCM answers UNREGISTERED, SENDER_ID_MISMATCH, or INVALID_ARGUMENT about the registration token, SignOps removes that device and records the reason. A user keeps at most 20 devices; a 21st drops the one seen longest ago. SignOps sends to FCM in the token field, which Firebase says also accepts an installation ID during the transition.
The app side is still yours: upload the token on each start, as above. Setup is in the Android push guide.
Sources: Firebase: Best practices for FCM registration management, Firebase: Set up an FCM client app on Android, Firebase: Set up an FCM client app on Apple platforms, Firebase: Get started with FCM in a web app, Firebase: FCM HTTP v1 message.