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)
}
}
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
)
}