This error occurs when Firebase Cloud Messaging attempts to send a message to a registration token that is no longer valid or has been unregistered. The token may have expired due to inactivity, the user may have uninstalled the app, or the app data may have been cleared. The fix involves removing invalid tokens from your database and implementing proper token lifecycle management.
The "messaging/registration-token-not-registered" error indicates that the Firebase Cloud Messaging (FCM) server rejected your message because the provided registration token is not registered or has expired. Unlike the "invalid-registration-token" error which means the token string itself is malformed, this error means the token was once valid but is no longer usable. FCM registration tokens are unique identifiers assigned to each device/app combination when the app first registers with Firebase. These tokens are designed to be long-lived, but they can become unregistered for several legitimate reasons related to app lifecycle and device state. This is one of the most common FCM errors in production, and it's expected that some tokens will occasionally become invalid. The key is to handle these errors gracefully by removing stale tokens from your database rather than repeatedly trying to send to them.
When you receive this error, the token is definitely no longer valid. Remove it from your database to prevent repeated failures and improve application performance.
// Node.js Admin SDK
async function sendNotificationWithErrorHandling(userId, message) {
const user = await db.users.findUnique({
where: { id: userId },
select: { fcmToken: true }
});
if (!user.fcmToken) {
console.log('User has no FCM token');
return;
}
try {
const response = await admin.messaging().send({
token: user.fcmToken,
notification: {
title: message.title,
body: message.body
}
});
console.log('Notification sent:', response);
return response;
} catch (error) {
// Check for registration token errors
if (error.code === 'messaging/registration-token-not-registered' ||
error.code === 'messaging/invalid-registration-token') {
console.log('Token is no longer valid, removing:', user.fcmToken);
// Delete the invalid token
await db.users.update({
where: { id: userId },
data: {
fcmToken: null,
fcmTokenInvalidatedAt: new Date(),
fcmTokenInvalidReason: error.code
}
});
console.log('Invalid token removed for user:', userId);
return null;
}
throw error;
}
}For batch sends, handle individual token failures:
// Send to multiple users
const response = await admin.messaging().sendEachForMulticast({
tokens: registrationTokens,
notification: {
title: 'Notification Title',
body: 'Notification body'
}
});
// Process failures
const tokensToDelete = [];
response.responses.forEach((resp, index) => {
if (!resp.success) {
const error = resp.error;
console.error(`Send to token ${index} failed:`, error.code);
// Mark for deletion
if (error.code === 'messaging/registration-token-not-registered' ||
error.code === 'messaging/invalid-registration-token') {
tokensToDelete.push(registrationTokens[index]);
}
}
});
// Bulk delete invalid tokens
if (tokensToDelete.length > 0) {
await db.users.updateMany({
where: {
fcmToken: { in: tokensToDelete }
},
data: {
fcmToken: null,
fcmTokenInvalidatedAt: new Date()
}
});
console.log(`Removed ${tokensToDelete.length} invalid tokens`);
}
console.log(`Send result: ${response.successCount} succeeded, ${response.failureCount} failed`);Ensure your client app properly handles token refresh events and sends updated tokens back to your server. FCM can refresh tokens periodically, and if your app doesn't notify the server of the new token, you'll quickly accumulate stale tokens in your database.
Web (JavaScript/TypeScript):
import { getMessaging, onMessage } from 'firebase/messaging';
import { initializeApp } from 'firebase/app';
const app = initializeApp(firebaseConfig);
const messaging = getMessaging(app);
// Listen for token refresh
messaging.onTokenRefresh(() => {
getToken(messaging, { vapidKey: 'YOUR_VAPID_KEY' })
.then(newToken => {
console.log('FCM Token refreshed:', newToken);
// Send new token to server
fetch('/api/update-fcm-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: newToken })
})
.catch(err => console.error('Failed to update token:', err));
});
});Android (Kotlin):
class MyFirebaseMessagingService : FirebaseMessagingService() {
override fun onNewToken(token: String) {
super.onNewToken(token)
Log.d(TAG, "Refreshed token: $token")
// Send to your backend
sendTokenToServer(token)
}
private fun sendTokenToServer(token: String) {
val apiService = RetrofitClient.getInstance().create(ApiService::class.java)
apiService.updateFcmToken(UpdateTokenRequest(token))
.enqueue(object : Callback<Void> {
override fun onResponse(call: Call<Void>, response: Response<Void>) {
Log.d(TAG, "Token sent to server successfully")
}
override fun onFailure(call: Call<Void>, t: Throwable) {
Log.e(TAG, "Failed to send token to server", t)
}
})
}
}iOS (Swift):
import Firebase
import FirebaseMessaging
class AppDelegate: UIResponder, UIApplicationDelegate, MessagingDelegate {
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
if let token = fcmToken {
print("FCM token refreshed: \(token)")
// Send to server
sendTokenToServer(token)
}
}
private func sendTokenToServer(_ token: String) {
let urlString = "https://yourserver.com/api/update-fcm-token"
guard let url = URL(string: urlString) else { return }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try? JSONSerialization.data(
withJSONObject: ["token": token]
)
URLSession.shared.dataTask(with: request).resume()
}
}Check token age and validity before attempting to send. Implement a system to proactively refresh or remove tokens that may be stale.
// Token validation utility
function shouldSkipToken(tokenRecord) {
// Skip if token is NULL
if (!tokenRecord.fcmToken) {
return true;
}
// Skip if token was invalidated recently
if (tokenRecord.fcmTokenInvalidatedAt) {
return true;
}
// Skip if token has not been updated in 30 days (potential stale token)
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
if (new Date(tokenRecord.fcmTokenUpdatedAt) < thirtyDaysAgo) {
console.warn('Token not updated in 30 days, skipping:', tokenRecord.id);
return true;
}
return false;
}
// Before sending notifications
async function sendBulkNotifications(userIds, message) {
const users = await db.users.findMany({
where: { id: { in: userIds } },
select: {
id: true,
fcmToken: true,
fcmTokenUpdatedAt: true,
fcmTokenInvalidatedAt: true
}
});
const validTokens = users
.filter(user => !shouldSkipToken(user))
.map(user => user.fcmToken);
if (validTokens.length === 0) {
console.log('No valid tokens available');
return;
}
const response = await admin.messaging().sendEachForMulticast({
tokens: validTokens,
notification: message
});
console.log(`Sent to ${response.successCount} devices, ${response.failureCount} failed`);
}Create an API endpoint that allows clients to update their FCM token when it changes. This should be called both at app startup and whenever the token is refreshed.
// Express.js example
app.post('/api/update-fcm-token', authenticateUser, async (req, res) => {
try {
const { token } = req.body;
const userId = req.user.id;
// Validate token format (FCM tokens are typically 150+ chars)
if (!token || typeof token !== 'string' || token.length < 100) {
return res.status(400).json({
error: 'Invalid FCM token format'
});
}
// Update user's token
const updated = await db.users.update({
where: { id: userId },
data: {
fcmToken: token,
fcmTokenUpdatedAt: new Date(),
fcmTokenInvalidatedAt: null,
fcmTokenInvalidReason: null
}
});
console.log('Token updated for user:', userId);
res.json({
success: true,
message: 'Token updated',
tokenLength: token.length
});
} catch (error) {
console.error('Token update failed:', error);
res.status(500).json({
error: 'Failed to update token',
message: error.message
});
}
});
// Next.js API route example
export async function POST(request: NextRequest) {
try {
const session = await getSession(request);
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { token } = await request.json();
if (!token || token.length < 100) {
return NextResponse.json(
{ error: 'Invalid token format' },
{ status: 400 }
);
}
await prisma.user.update({
where: { id: session.user.id },
data: {
fcmToken: token,
fcmTokenUpdatedAt: new Date()
}
});
return NextResponse.json({ success: true });
} catch (error) {
return NextResponse.json(
{ error: 'Failed to update token' },
{ status: 500 }
);
}
}Make sure this endpoint is called:
- On app startup (to ensure server has current token)
- When token refresh is detected (via onTokenRefresh)
- After user login (token might have changed)
Implement scheduled tasks to detect and remove tokens that haven't been updated in a long time, preventing accumulation of dead tokens.
// Scheduled job to clean up stale tokens (run daily/weekly)
async function cleanupStaleTokens() {
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
const staleTokenCount = await db.users.updateMany({
where: {
fcmToken: { not: null },
fcmTokenUpdatedAt: { lt: thirtyDaysAgo }
},
data: {
fcmToken: null,
fcmTokenInvalidatedAt: new Date(),
fcmTokenInvalidReason: 'AUTO_CLEANUP_STALE'
}
});
console.log(`Cleaned up ${staleTokenCount.count} stale tokens`);
return staleTokenCount.count;
}
// Bull Queue example for scheduled job
const cleanupQueue = new Queue('token-cleanup', {
connection: redis
});
cleanupQueue.process(async () => {
return cleanupStaleTokens();
});
// Schedule to run daily at 2 AM
cleanupQueue.add(
{},
{
repeat: {
cron: '0 2 * * *'
}
}
);
// Node.js cron example
import cron from 'node-cron';
// Run cleanup every Sunday at 3 AM
cron.schedule('0 3 * * 0', async () => {
console.log('Running scheduled token cleanup...');
try {
const removed = await cleanupStaleTokens();
console.log(`Cleanup completed: removed ${removed} tokens`);
} catch (error) {
console.error('Cleanup failed:', error);
}
});You can also add logging to track token invalidation patterns:
// Add to your error handling
async function logTokenError(userId, token, errorCode) {
await db.tokenErrors.create({
data: {
userId,
token: token.substring(0, 20) + '...', // Log partial token for privacy
errorCode,
timestamp: new Date(),
deviceInfo: userAgent
}
});
}
// Use in error handler
if (error.code === 'messaging/registration-token-not-registered') {
await logTokenError(userId, token, error.code);
// ... delete token
}Token lifecycle on different platforms: Android tokens can go stale after 30+ days of inactivity, while iOS tokens depend on APNS certificate validity. Web tokens (using VAPID) are browser-specific and will regenerate when cookies/storage is cleared. Implement platform-aware token management.
Batch send error patterns: When using sendEachForMulticast(), if a significant portion of tokens fail with "registration-token-not-registered", it may indicate app uninstalls are spiking (seasonal apps, version bugs, etc.). Monitor the failure rate as a health metric.
Token format confusion: The error code "messaging/registration-token-not-registered" is specifically for tokens that are syntactically valid but no longer registered. This is different from "messaging/invalid-registration-token" (malformed token string). Both should result in token deletion, but "not-registered" suggests the user likely uninstalled the app or cleared app data.
GDPR and token deletion: Deleting tokens from your database when they become invalid is also GDPR-compliant (you're not retaining data for users who've uninstalled). However, if you're storing token error logs, ensure you have a retention policy.
Database index optimization: If you frequently query fcmToken to find users by token or update them by token status, add indexes:
CREATE INDEX idx_users_fcm_token ON users(fcm_token);
CREATE INDEX idx_users_fcm_updated_at ON users(fcm_token_updated_at);
CREATE INDEX INDEX idx_users_fcm_invalidated ON users(fcm_token_invalidated_at);Metrics to track: Monitor these metrics to catch FCM delivery issues early:
- Percentage of tokens that fail with "not-registered" vs. other errors
- Token refresh frequency and response times
- Average age of valid tokens in your database
- Time to cleanup after token invalidation
Callable Functions: INTERNAL - Unhandled exception
How to fix "Callable Functions: INTERNAL - Unhandled exception" in Firebase
auth/invalid-hash-algorithm: Hash algorithm doesn't match supported options
How to fix "auth/invalid-hash-algorithm: Hash algorithm doesn't match supported options" in Firebase
Hosting: CORS configuration not set up properly
How to fix CORS configuration in Firebase Hosting
auth/reserved-claims: Custom claims use reserved OIDC claim names
How to fix "reserved claims" error when setting custom claims in Firebase
Callable Functions: UNAUTHENTICATED - Invalid credentials
How to fix "UNAUTHENTICATED - Invalid credentials" in Firebase Callable Functions