This error occurs when Firebase Cloud Messaging detects that a registration token was once valid but has become inactive. The token typically becomes inactive after the app is uninstalled, user data is cleared, the device remains inactive for extended periods, or after a token refresh cycle. Fixing this requires removing stale tokens and implementing proper token lifecycle management.
The "messaging/registration-token-not-registered: Previously valid token now inactive" error is a specific variant of the registration-token-not-registered error. It explicitly indicates that FCM previously recognized this token as valid but it has since become inactive and unusable. This error message is particularly informative because it confirms the token was never malformed or fake—it was legitimately registered at some point. The inactivity status means the token's registration has expired or been invalidated by FCM due to device or app state changes. This is a normal part of FCM's token lifecycle and is expected in production applications. The key to handling this gracefully is to remove these tokens immediately and ensure your system has mechanisms to obtain fresh tokens from active users.
When you receive the "previously valid token now inactive" error, the token cannot be reactivated. Remove it from your database immediately to prevent repeated failed sends and improve application efficiency.
// Node.js Admin SDK - Single token deletion
async function handleInactiveToken(userId, fcmToken) {
try {
const response = await admin.messaging().send({
token: fcmToken,
notification: {
title: 'Test',
body: 'Test'
}
});
} catch (error) {
if (error.code === 'messaging/registration-token-not-registered') {
console.log('Token is inactive, removing from database:', fcmToken);
// Delete the inactive token
await prisma.user.update({
where: { id: userId },
data: {
fcmToken: null,
fcmTokenStatus: 'INACTIVE',
fcmTokenInactivatedAt: new Date()
}
});
return { success: false, reason: 'Token removed - was inactive' };
}
throw error;
}
}
// Batch send with inactive token detection
async function sendBatchNotifications(recipients, message) {
const response = await admin.messaging().sendEachForMulticast({
tokens: recipients.map(r => r.fcmToken),
notification: message
});
// Handle failures for inactive tokens
const inactiveTokens = [];
response.responses.forEach((resp, index) => {
if (!resp.success && resp.error.code === 'messaging/registration-token-not-registered') {
const token = recipients[index].fcmToken;
console.warn('Inactive token detected:', token.substring(0, 20) + '...');
inactiveTokens.push({
userId: recipients[index].id,
token: token
});
}
});
// Bulk delete all inactive tokens
if (inactiveTokens.length > 0) {
await prisma.user.updateMany({
where: {
fcmToken: { in: inactiveTokens.map(t => t.token) }
},
data: {
fcmToken: null,
fcmTokenStatus: 'INACTIVE',
fcmTokenInactivatedAt: new Date()
}
});
console.log(`Removed ${inactiveTokens.length} inactive tokens`);
}
return {
successCount: response.successCount,
failureCount: response.failureCount,
inactiveTokensRemoved: inactiveTokens.length
};
}Important: Don't retry sends to inactive tokens. Each retry wastes resources and API quota. Delete them once detected.
Ensure your client app captures token refresh events and immediately sends the new token to your server. Inactive tokens often result from the app losing sync with the server about which token is current.
Web (JavaScript):
import { getMessaging, getToken, onMessage } from 'firebase/messaging';
import { initializeApp } from 'firebase/app';
const app = initializeApp(firebaseConfig);
const messaging = getMessaging(app);
async function registerFCMToken() {
try {
const token = await getToken(messaging, {
vapidKey: 'YOUR_VAPID_KEY'
});
if (token) {
console.log('FCM Token obtained:', token);
// Send to server immediately
await fetch('/api/update-fcm-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token: token,
timestamp: new Date().toISOString()
})
});
}
} catch (error) {
console.error('Failed to get FCM token:', error);
}
}
// Call on app initialization
registerFCMToken();
// Listen for token refresh (important!)
messaging.onTokenRefresh(async () => {
console.log('FCM token refreshed');
await registerFCMToken();
});Android (Kotlin):
class MyFirebaseMessagingService : FirebaseMessagingService() {
override fun onNewToken(token: String) {
super.onNewToken(token)
Log.d("FCM", "Token refreshed: $token")
// Send immediately to server
sendTokenToServer(token)
// Also save to SharedPreferences as backup
saveTokenLocally(token)
}
private fun sendTokenToServer(token: String) {
val retrofit = RetrofitClient.getInstance()
val apiService = retrofit.create(ApiService::class.java)
apiService.updateFcmToken(FcmTokenRequest(token))
.enqueue(object : Callback<Void> {
override fun onResponse(call: Call<Void>, response: Response<Void>) {
Log.d("FCM", "Token sent to server: ${response.code()}")
}
override fun onFailure(call: Call<Void>, t: Throwable) {
Log.e("FCM", "Failed to send token", t)
// Retry later or queue for sync
}
})
}
private fun saveTokenLocally(token: String) {
val prefs = getSharedPreferences("fcm", Context.MODE_PRIVATE)
prefs.edit().putString("fcm_token", token).apply()
}
}iOS (Swift):
import Firebase
import FirebaseMessaging
class AppDelegate: UIResponder, UIApplicationDelegate, MessagingDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
FirebaseApp.configure()
Messaging.messaging().delegate = self
// Get initial token
Messaging.messaging().token { token, error in
if let token = token {
print("FCM Token: \(token)")
self.sendTokenToServer(token)
}
}
return true
}
func messaging(_ messaging: Messaging,
didReceiveRegistrationToken fcmToken: String?) {
if let token = fcmToken {
print("Token refreshed: \(token)")
sendTokenToServer(token)
}
}
private func sendTokenToServer(_ token: String) {
let request = URLRequest(url: URL(string: "https://yourapp.com/api/update-fcm-token")!)
var request = request
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try? JSONSerialization.data(withJSONObject: ["token": token])
URLSession.shared.dataTask(with: request).resume()
}
}Your clients need a reliable way to send updated tokens to your server. This endpoint should validate tokens and update the database.
// Express.js
import express from 'express';
import { authenticateUser } from './auth';
import { prisma } from './db';
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 150+ chars)
if (!token || typeof token !== 'string' || token.length < 100) {
return res.status(400).json({
error: 'Invalid token format',
receivedLength: token?.length
});
}
// Update user's token
const user = await prisma.user.update({
where: { id: userId },
data: {
fcmToken: token,
fcmTokenUpdatedAt: new Date(),
fcmTokenStatus: 'ACTIVE',
fcmTokenInactivatedAt: null
},
select: { id: true, email: true }
});
console.log(`Token updated for user ${user.email}`);
return res.json({
success: true,
message: 'FCM token updated successfully',
tokenHash: token.substring(0, 10) + '...'
});
} catch (error) {
console.error('Token update error:', error);
return res.status(500).json({
error: 'Failed to update token',
message: error.message
});
}
});
// Next.js API Route
import { NextRequest, NextResponse } from 'next/server';
import { getSession } from '@/lib/auth';
import { prisma } from '@/lib/db';
export async function POST(request: NextRequest) {
try {
const session = await getSession(request);
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { token } = await request.json();
if (!token || typeof token !== 'string' || 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(),
fcmTokenStatus: 'ACTIVE'
}
});
return NextResponse.json({
success: true,
message: 'Token updated'
});
} catch (error) {
console.error('Token update failed:', error);
return NextResponse.json(
{ error: 'Token update failed' },
{ status: 500 }
);
}
}Call this endpoint:
- When user logs in
- On app startup (to sync server with current token)
- Whenever token refresh is detected
- Periodically (e.g., every 24 hours) to ensure sync
Prevent accumulating inactive tokens by checking how long it's been since a token was last updated. Inactive tokens are often very old.
async function shouldSendToToken(userRecord) {
// Skip null tokens
if (!userRecord.fcmToken) return false;
// Skip if explicitly marked inactive
if (userRecord.fcmTokenStatus === 'INACTIVE') return false;
// Skip if recently marked as inactive
if (userRecord.fcmTokenInactivatedAt) return false;
// Skip if token hasn't been updated in 30+ days (likely inactive)
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
if (userRecord.fcmTokenUpdatedAt < thirtyDaysAgo) {
console.warn(`Skipping token not updated in 30+ days for user ${userRecord.id}`);
return false;
}
return true;
}
async function sendNotifications(userIds, message) {
// Fetch users with their token info
const users = await prisma.user.findMany({
where: { id: { in: userIds } },
select: {
id: true,
fcmToken: true,
fcmTokenStatus: true,
fcmTokenUpdatedAt: true,
fcmTokenInactivatedAt: true
}
});
// Filter out users with inactive/old tokens
const validUsers = users.filter(user => shouldSendToToken(user));
console.log(`Sending to ${validUsers.length} of ${users.length} users (skipped ${users.length - validUsers.length} with inactive tokens)`);
if (validUsers.length === 0) {
console.log('No users with active tokens');
return { success: true, sent: 0, skipped: users.length };
}
// Send to valid tokens
const response = await admin.messaging().sendEachForMulticast({
tokens: validUsers.map(u => u.fcmToken),
notification: message
});
return {
success: true,
sent: response.successCount,
failed: response.failureCount,
skipped: users.length - validUsers.length
};
}Implement a scheduled task to detect and remove tokens that have been inactive for extended periods, preventing database bloat and reducing lookup times.
// Clean up tokens not updated in 30 days
async function cleanupInactiveTokens() {
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
const result = await prisma.user.updateMany({
where: {
fcmToken: { not: null },
fcmTokenUpdatedAt: { lt: thirtyDaysAgo }
},
data: {
fcmToken: null,
fcmTokenStatus: 'INACTIVE',
fcmTokenInactivatedAt: new Date()
}
});
console.log(`Cleaned up ${result.count} inactive tokens`);
return result.count;
}
// Using node-cron for scheduling
import cron from 'node-cron';
// Run every day at 2 AM
cron.schedule('0 2 * * *', async () => {
console.log('Running FCM token cleanup...');
try {
const cleaned = await cleanupInactiveTokens();
console.log(`Cleanup completed: ${cleaned} tokens removed`);
} catch (error) {
console.error('Cleanup failed:', error);
}
});
// Using Bull Queue for more robust scheduling
import Bull from 'bull';
const cleanupQueue = new Bull('fcm-cleanup', {
redis: { host: 'localhost', port: 6379 }
});
cleanupQueue.process(async (job) => {
return cleanupInactiveTokens();
});
// Schedule to run daily
cleanupQueue.add(
{},
{
repeat: {
cron: '0 2 * * *', // 2 AM daily
timezone: 'UTC'
}
}
);
// Airflow or other orchestration
// Add as a daily DAG task to clean up inactive tokensYou can also log which tokens become inactive for analytics:
async function recordTokenInactivity(userId, token, reason) {
await prisma.fcmTokenLog.create({
data: {
userId,
tokenPreview: token.substring(0, 20) + '...',
reason: reason,
timestamp: new Date()
}
});
}
// Use in your error handling
catch (error) {
if (error.code === 'messaging/registration-token-not-registered') {
await recordTokenInactivity(userId, token, 'SEND_FAILED');
// ... delete token
}
}Token inactivity vs. expiration: FCM doesn't have explicit token expiration dates like JWT tokens. Instead, tokens become "inactive" when FCM detects the device/app is no longer actively connected (inactivity, uninstall, etc.). This is better for user privacy—tokens don't "expire"—they just become invalid naturally.
Platform-specific inactivity timelines: Android and web tokens typically go inactive after 30+ days of device inactivity. iOS tokens can become inactive when the APNS certificate expires or the device loses connectivity to Apple's push service. Always assume any token older than 30 days of inactivity may be inactive.
Batch send failure analysis: If you see a sudden spike in "previously valid token now inactive" errors during a batch send, it may indicate:
- Mass app uninstalls (new version bug, security issue, etc.)
- Device updates that invalidated tokens (OS update, factory reset wave)
- Your app lost token refresh capability (regression in recent release)
Track the percentage of tokens failing and alert if it exceeds 10% in a single batch.
Database schema for token tracking: Recommended fields to track token lifecycle:
model User {
id String @id @default(cuid())
// FCM Token management
fcmToken String? @unique
fcmTokenStatus String @default("ACTIVE") // ACTIVE, INACTIVE, INVALID
fcmTokenUpdatedAt DateTime?
fcmTokenInactivatedAt DateTime?
fcmTokenInactivationReason String?
}
model FcmTokenLog {
id String @id @default(cuid())
userId String
tokenPreview String // First 20 chars for privacy
reason String // SEND_FAILED, AUTO_CLEANUP, USER_UNINSTALL, etc.
timestamp DateTime @default(now())
@@index([userId])
@@index([timestamp])
}Privacy considerations: When logging token inactivity, never store complete tokens. Use just the first 20 characters for privacy and security. Also implement retention policies (delete logs after 30 days) to comply with GDPR.
Metrics to monitor for early warning:
- Daily percentage of inactive token errors vs. total sends
- Average token age in your database
- Days since last token update for users who still have tokens stored
- Token update success rate (can indicate a regression in client code)
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