The Firebase Cloud Messaging "messaging/invalid-argument" error occurs when FCM rejects a send request due to invalid parameters, malformed tokens, oversized payloads, or incorrect message structure. This is a critical error that prevents messages from being delivered to devices.
The "messaging/invalid-argument" error in Firebase Cloud Messaging (FCM) is returned by the FCM service when a message request violates one of FCM's validation rules. Unlike client-side parameter errors, this error comes from the FCM API server after validating the complete request. This error can indicate several different problems depending on context: invalid device registration tokens, message payloads that exceed size limits, sending to more than 1000 tokens in a single request, incorrect message structure for the target platform, or invalid priority settings for specific device types. The error code "messaging/invalid-argument" is a broad error category that requires examining the error message details and your request parameters to determine the root cause. Each cause requires a different fix approach.
The most common cause of this error is an invalid or incorrectly formatted registration token. Check your token format:
const registrationToken = '...'; // Should be ~152 characters
// Check token format
if (!registrationToken || registrationToken.length < 100) {
console.error('Token appears invalid - too short or missing');
}
// Token format validation
const fcmTokenRegex = /^[a-zA-Z0-9_-]{100,200}$/;
if (!fcmTokenRegex.test(registrationToken)) {
console.warn('Token format may be invalid');
}
// Verify token before sending
console.log('Token length:', registrationToken.length); // Should be ~152
console.log('Token start:', registrationToken.substring(0, 20));If the token is invalid, stale, or malformed:
1. Request a fresh token from the client device
2. Delete the invalid token from your database
3. Ask the user to reinstall the app if the token won't refresh
4. Implement token refresh logic with expiration handling
FCM has a hard limit of 1000 registration tokens per send request. Split larger batches:
const registrationTokens = [/* array of tokens */];
// Check batch size
if (registrationTokens.length > 1000) {
console.warn(`Batch size is ${registrationTokens.length} - exceeds FCM limit of 1000`);
}
// Correct way to send to 1000+ devices
async function sendToLargeDeviceSet(tokens, message) {
const BATCH_SIZE = 500; // Use smaller batches for reliability
for (let i = 0; i < tokens.length; i += BATCH_SIZE) {
const batch = tokens.slice(i, i + BATCH_SIZE);
try {
const response = await admin.messaging().sendMulticast({
tokens: batch,
notification: message.notification,
data: message.data,
android: message.android,
apns: message.apns,
webpush: message.webpush
});
console.log(`Batch ${Math.floor(i / BATCH_SIZE) + 1}: Success: ${response.successCount}, Failed: ${response.failureCount}`);
// Handle failures
response.responses.forEach((resp, index) => {
if (!resp.success && resp.error) {
const token = batch[index];
console.error(`Failed to send to ${token}: ${resp.error.code}`);
// Delete invalid tokens
if (resp.error.code === 'messaging/invalid-registration-token' ||
resp.error.code === 'messaging/invalid-argument') {
// Delete from your database
await deleteTokenFromDatabase(token);
}
}
});
} catch (error) {
console.error(`Batch ${Math.floor(i / BATCH_SIZE) + 1} failed:`, error);
}
}
}
// Send to device set
await sendToLargeDeviceSet(registrationTokens, message);Always batch requests to 500-1000 tokens to avoid hitting limits.
Firebase has strict payload size limits. Check your message size before sending:
const message = {
token: registrationToken,
notification: {
title: 'Notification Title',
body: 'Notification body text'
},
data: {
key1: 'value1',
key2: 'value2'
}
};
// Check payload size
function checkMessageSize(message) {
const serialized = JSON.stringify(message);
const bytes = new TextEncoder().encode(serialized).length;
console.log(`Total payload size: ${bytes} bytes`);
console.log('FCM limits:');
console.log(' - Notification: 4000 bytes');
console.log(' - Data object: 4000 bytes');
console.log(' - Total message: 4096 bytes');
if (bytes > 4096) {
console.error('ERROR: Message exceeds 4096 byte limit');
return false;
}
if (message.notification) {
const notifSize = JSON.stringify(message.notification).length;
if (notifSize > 4000) {
console.error('ERROR: Notification payload exceeds 4000 bytes');
return false;
}
}
if (message.data) {
const dataSize = JSON.stringify(message.data).length;
if (dataSize > 4000) {
console.error('ERROR: Data payload exceeds 4000 bytes');
return false;
}
}
return true;
}
// Validate before sending
if (!checkMessageSize(message)) {
console.error('Message is too large - reduce content or split into multiple messages');
// Truncate or remove non-essential data
message.data = message.data ? { /* essential fields only */ } : undefined;
}
await admin.messaging().send(message);Keep payloads small:
- Notification title/body: maximum 240 characters combined
- Data values: keep each value under 1000 characters
- Total message: under 4096 bytes
iOS and APNs have strict priority requirements. High priority data messages are rejected:
const message = {
token: apnsToken,
notification: {
title: 'Title',
body: 'Body'
},
data: {
customKey: 'customValue'
}
};
// WRONG: High priority for data-only message on iOS
// await messaging.send({
// token: apnsToken,
// data: { key: 'value' },
// webpush: { headers: { TTL: '3600' } },
// // Missing apns, causes INVALID_ARGUMENT
// });
// CORRECT: Proper priority handling for different platforms
const correctMessage = {
token: apnsToken,
notification: {
title: 'Push Title',
body: 'Push Body'
},
data: {
action: 'open_details',
id: '123'
},
// For iOS/APNs: Priority MUST be normal (5) or omitted for silent
apns: {
headers: {
'apns-priority': '10' // 10 for alerts, 5 for background
},
payload: {
aps: {
alert: {
title: 'Push Title',
body: 'Push Body'
},
sound: 'default',
'content-available': 0 // Set to 1 only for silent notifications
}
}
},
// For Android: Can use high priority
android: {
priority: 'high', // Only use high for time-sensitive messages
notification: {
title: 'Push Title',
body: 'Push Body'
}
}
};
await admin.messaging().send(correctMessage);
// Platform-specific sending
async function sendPlatformSpecific(token, isIOS) {
if (isIOS) {
// iOS: Use normal priority, support content-available
return messaging.send({
token,
notification: { title: 't', body: 'b' },
apns: {
headers: { 'apns-priority': '10' },
payload: { aps: { sound: 'default' } }
}
});
} else {
// Android: Can use high priority
return messaging.send({
token,
notification: { title: 't', body: 'b' },
android: { priority: 'high' }
});
}
}Key rules:
- iOS high-priority messages must include a notification
- Data-only messages to iOS must use normal priority (apns-priority: 5)
- Silent notifications (content-available) should not use high priority
- Android can use high priority for any message type
Stale tokens from inactive devices cause INVALID_ARGUMENT errors. Implement token lifecycle management:
// Track token age
interface TokenRecord {
token: string;
userId: string;
createdAt: Date;
lastUsedAt: Date;
platform: 'ios' | 'android' | 'web';
}
// Mark tokens as stale if inactive for 270+ days
async function removeStaleTokens() {
const STALE_THRESHOLD = 270 * 24 * 60 * 60 * 1000; // 270 days in ms
const now = new Date();
const staleTokens = await db.tokens.find({
lastUsedAt: {
$lt: new Date(now - STALE_THRESHOLD)
}
});
console.log(`Found ${staleTokens.length} stale tokens`);
// Delete stale tokens
for (const token of staleTokens) {
await db.tokens.deleteOne({ token: token.token });
}
}
// Handle token errors during send
async function sendWithTokenCleanup(token, message) {
try {
return await admin.messaging().send({
token,
...message
});
} catch (error) {
// Delete invalid or unregistered tokens
if (error.code === 'messaging/invalid-registration-token' ||
error.code === 'messaging/registration-token-not-registered' ||
error.code === 'messaging/invalid-argument') {
console.log(`Deleting invalid token: ${token}`);
await db.tokens.deleteOne({ token });
// Notify user or re-request token
await db.users.updateOne(
{ tokens: token },
{ $pull: { tokens: token } }
);
}
throw error;
}
}
// Periodic cleanup
setInterval(removeStaleTokens, 24 * 60 * 60 * 1000); // Daily
// On successful send, update lastUsedAt
async function sendAndTrack(token, message) {
const result = await sendWithTokenCleanup(token, message);
await db.tokens.updateOne(
{ token },
{ lastUsedAt: new Date() }
);
return result;
}Implement:
- Track token creation and last used dates
- Delete tokens older than 270 days
- Delete tokens immediately after INVALID_ARGUMENT errors
- Request fresh tokens periodically from clients
Get full error details to pinpoint the exact cause:
async function debugInvalidArgument(token, message) {
try {
const response = await admin.messaging().send({
token,
...message
});
console.log('Send successful:', response);
} catch (error) {
console.error('FCM Error Details:');
console.error(' Code:', error.code); // messaging/invalid-argument
console.error(' Message:', error.message); // Full error message
console.error(' Full error:', JSON.stringify(error, null, 2));
// Check error.errorInfo for FCM-specific details
if (error.errorInfo) {
console.error(' Error Info:', error.errorInfo);
}
// Response from HTTP v1 API includes details array
if (error.cause && error.cause.response) {
console.error(' Response status:', error.cause.response.status);
console.error(' Response data:', error.cause.response.data);
const { details } = error.cause.response.data.error;
if (details && details.length > 0) {
console.error(' FCM details:', details);
details.forEach(detail => {
console.error(' -', detail['@type'], ':', detail);
});
}
}
// Analyze the error message
const errorMsg = error.message || '';
if (errorMsg.includes('registration token')) {
console.error('ROOT CAUSE: Invalid registration token');
} else if (errorMsg.includes('data too large')) {
console.error('ROOT CAUSE: Message payload exceeds limits');
} else if (errorMsg.includes('1000')) {
console.error('ROOT CAUSE: Batch size exceeds 1000 tokens');
} else if (errorMsg.includes('priority') || errorMsg.includes('high')) {
console.error('ROOT CAUSE: Invalid priority for message type/platform');
}
}
}
// Test with sample data
const testToken = 'your-test-token-here';
const testMessage = {
notification: { title: 'Test', body: 'Message' }
};
await debugInvalidArgument(testToken, testMessage);The error message often contains hints:
- "registration token" → invalid/expired token
- "data too large" → payload exceeds 4096 bytes
- "1000" → batch too large
- "priority" → incorrect priority for platform
- "APNs" → iOS-specific validation issue
### Firebase Cloud Messaging Validation Rules
Token Validation:
- Tokens must be 100-152 characters
- Tokens expire after 270 days of inactivity on Android
- iOS tokens can become invalid if app is uninstalled
- Web tokens expire based on service worker registration status
- Testing tokens (if using FCM emulator) have different formats
Payload Limits:
- Total message: 4096 bytes maximum
- Notification object: 4000 bytes maximum
- Data object: 4000 bytes maximum
- Single data value: no hard limit but keep under 1000 characters
- Notification title: 240 characters recommended maximum
Platform-Specific Requirements:
- iOS (APNs): High priority (apns-priority: 10) requires notification, silent notifications use normal priority
- Android: High priority bypasses doze mode, triggers immediate delivery
- Web (Webpush): TTL header controls message retention (in seconds)
- Multiple platforms: Each platform gets its own config to avoid INVALID_ARGUMENT
Message Structure Requirements:
- Must have either token, topic, or condition
- tokens array for sendMulticast (max 500-1000 tokens)
- condition for topic-based or complex logical conditions
- Message must include at least notification OR data (not empty)
### Common Patterns by Platform
Android Specific:
const androidMessage = {
token: androidToken,
notification: {
title: 'Title',
body: 'Body',
tag: 'notification-tag',
color: '#ff0000'
},
android: {
priority: 'high', // high or normal
ttl: 3600,
restrictedPackageName: 'com.your.app'
}
};iOS Specific:
const iosMessage = {
token: iosToken,
notification: {
title: 'Title',
body: 'Body'
},
apns: {
headers: {
'apns-priority': '10', // 10 for alert, 5 for background
'apns-expiration': String(Math.floor(Date.now() / 1000) + 3600)
},
payload: {
aps: {
alert: {
title: 'Title',
body: 'Body'
},
sound: 'default',
'content-available': 0 // 1 for silent notifications
}
}
}
};Web Specific:
const webMessage = {
token: webToken,
notification: {
title: 'Title',
body: 'Body',
icon: 'https://example.com/icon.png'
},
webpush: {
headers: {
TTL: '3600' // Seconds to retain message
},
data: {
url: 'https://example.com/page'
}
}
};### Error Recovery Patterns
Implement exponential backoff for transient failures:
async function sendWithRetry(token, message, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await admin.messaging().send({ token, ...message });
} catch (error) {
if (error.code === 'messaging/invalid-argument') {
// Don't retry invalid argument errors
throw error;
}
const delay = Math.pow(2, attempt) * 1000; // Exponential backoff
console.log(`Attempt ${attempt + 1} failed, retrying in ${delay}ms`);
await new Promise(r => setTimeout(r, delay));
}
}
throw new Error('Max retries exceeded');
}Monitor and alert on INVALID_ARGUMENT spikes:
const errorMetrics = {
'messaging/invalid-argument': 0,
'messaging/registration-token-not-registered': 0,
'messaging/third-party-auth-error': 0
};
async function trackSendError(error) {
const errorCode = error.code;
if (errorCode in errorMetrics) {
errorMetrics[errorCode]++;
// Alert if error rate is too high
if (errorMetrics[errorCode] > 100) {
console.warn(`High error rate for ${errorCode}`);
// Send alert, page on-call, etc.
}
}
}### Testing Strategies
Use Firebase Emulator Suite for local testing without risking real tokens:
firebase emulators:start --only messagingThis helps identify invalid-argument issues before production deployment.
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