androidspring-bootfirebasefirebase-cloud-messagingfirebase-admin

How to Handle UNREGISTERED Tokens When Using Firebase Topics for Notifications


I’m using Firebase's Topic feature to send notifications to multiple devices in my Spring Boot project. I use the firebase-admin library for server-side integration.

The issue is that when I send notifications via a topic, I don’t get any information about devices with invalid or UNREGISTERED tokens. This makes it difficult to manage inactive devices in my database.

I know that sending notifications to individual devices using their tokens would return errors like UNREGISTERED, which allows me to clean up my database.

However, with 100,000+ devices, sending notifications one by one seems inefficient and not scalable.

Is there a way to handle UNREGISTERED tokens when using topic-based notifications? If not, is sending notifications individually to a large number of devices (e.g., 100,000+) a recommended practice? Are there any best practices for managing token validity when using topics for notifications?

When send notifications to all devices in my database with topic:

  private fun sendToTopic(
        firebaseApp: FirebaseApp,
        title: String,
        body: String
    ): CustomPushNotificationResponse {
        val message = Message.builder()
            .setTopic(FCMUtils.TOPIC_LOGSNAIL)
            .setNotification(
                Notification.builder()
                    .setTitle(title)
                    .setBody(body)
                    .build()
            )
            .build()

        return try {
            val messageId = FirebaseMessaging.getInstance(firebaseApp).send(message)
            CustomPushNotificationResponse(isSuccess = true, messageId = messageId)
        } catch (e: FirebaseMessagingException) {
            CustomPushNotificationResponse(isSuccess = false, errorMessage = e.message)
        } catch (e: Exception) {
            CustomPushNotificationResponse(isSuccess = false, errorMessage = e.message)
        }
    }

When send notifications to specific device:

   private fun sendToDevice(
       firebaseApp: FirebaseApp,
       deviceUuid: String,
       title: String,
       body: String
   ): CustomPushNotificationResponse {
       val device = deviceRepository.findByDeviceUuid(deviceUuid)
           .orElseThrow { IllegalArgumentException("Device with UUID $deviceUuid not found") }

       val token = device.androidFirebaseToken
           ?: return CustomPushNotificationResponse(
               isSuccess = false,
               errorMessage = "Firebase token not available for device UUID $deviceUuid"
           )

       val message = Message.builder()
           .setToken(token)
           .setNotification(
               Notification.builder()
                   .setTitle(title)
                   .setBody(body)
                   .build()
           )
           .build()

       return try {
           val messageId = FirebaseMessaging.getInstance(firebaseApp).send(message)
           CustomPushNotificationResponse(isSuccess = true, messageId = messageId)
       } catch (e: FirebaseMessagingException) {
           handleUnregisteredToken(device, e)
       } catch (e: Exception) {
           CustomPushNotificationResponse(isSuccess = false, errorMessage = e.message)
       }
   }

Solution

  • I solved the problem with sendEach() instead of sendAll() which is a deprecated function. I send FCM tokens as a list with a maximum limit of 500 and it returns UNREGISTER statuses separately for all devices.

    According to this return answer, I make my devices isActive status false in the database.

    If you want to check the Success/Error status of each token, don't use topic.

        private fun sendBatchNotifications(
            firebaseApp: FirebaseApp,
            title: String?,
            body: String?
        ): CustomPushNotificationResponse {
            var page = 0
            val size = 490
            var successCount = 0
            var failureCount = 0
    
            do {
                val activeDevices = customDeviceRepository.fetchActiveDevicesPaginated(page, size)
                if (activeDevices.isEmpty()) break
    
                val tokenBatches = activeDevices.mapNotNull { it.androidFirebaseToken }
    
                val messages = tokenBatches.map { token ->
                    Message.builder().apply {
                        setToken(token)
                        title?.let {
                            setNotification(Notification.builder().setTitle(it).setBody(body).build())
                        }
                    }.build()
                }
    
                try {
                    val response: BatchResponse = FirebaseMessaging.getInstance(firebaseApp).sendEach(messages)
                    successCount += response.successCount
                    failureCount += response.failureCount
    
                    response.responses.forEachIndexed { index, resp ->
                        if (!resp.isSuccessful && resp.exception?.messagingErrorCode.toString() == UNREGISTERED) {
                            val token = tokenBatches[index]
                            customDeviceRepository.deactivateDeviceByFcmToken(token)
                        }
                    }
                } catch (e: Exception) {
                    e.printStackTrace()
                }
    
                page++
            } while (activeDevices.size == size)
    
            return CustomPushNotificationResponse(
                isSuccess = successCount > 0,
                successMessage = if(successCount > 0) "Notification sent to all active device." else null,
                errorMessage = if (failureCount > 0) "$failureCount notifications failed" else null
            )
        }