This error occurs when you exceed Firebase Cloud Messaging (FCM) rate limits while sending messages. Firebase enforces quota token buckets to prevent abuse and ensure fair resource allocation across all projects.
The `messaging/message-rate-exceeded` error indicates that your application has exceeded Firebase Cloud Messaging (FCM) rate limits. Firebase uses a token bucket system to manage the rate of messages that can be sent through the service. When sending messages via the FCM HTTP v1 API, each request consumes quota tokens from a "token bucket" that refills periodically. The HTTP v1 API allots 600,000 quota tokens for each 1-minute time window. When you exceed this limit, FCM returns HTTP status code 429 RESOURCE_EXHAUSTED with the error code "QUOTA_EXCEEDED". This error can manifest in different ways depending on your sending pattern: - Sending too many messages in a short burst (exceeding per-minute limits) - Sustained high-volume messaging over time - Topic-based messaging to large subscriber bases (which performs "fanout") - Device-specific messaging exceeding per-device limits (240 messages/minute for Android) The quota automatically refills at the end of each 1-minute window, but repeated violations suggest your messaging architecture needs optimization.
When you receive a 429 error, implement exponential backoff to retry after progressively longer intervals.
const admin = require('firebase-admin');
async function sendMessageWithRetry(message, options = {}) {
const maxRetries = options.maxRetries || 3;
const initialDelay = options.initialDelay || 1000; // Start with 1 second
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await admin.messaging().send(message);
console.log('Message sent successfully:', response);
return response;
} catch (error) {
if (error.code === 'messaging/message-rate-exceeded' ||
(error.codePrefix === 'messaging' && error.status === 429)) {
if (attempt < maxRetries - 1) {
const waitTime = Math.pow(2, attempt) * initialDelay;
// Add jitter to prevent thundering herd
const jitter = Math.random() * 0.1 * waitTime;
const totalWait = waitTime + jitter;
console.log(`Rate limited. Retrying in ${Math.round(totalWait)}ms (attempt ${attempt + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, totalWait));
continue;
}
}
// Non-retryable error or max retries exceeded
throw error;
}
}
throw new Error('Failed to send message after max retries');
}
// Usage
const message = {
notification: {
title: 'Hello',
body: 'World'
},
token: 'device_token_here'
};
await sendMessageWithRetry(message);This approach prevents temporary rate limit issues from causing permanent message failures. The jitter helps prevent multiple clients from retrying simultaneously.
Implement client-side rate limiting to stay well below Firebase's 600K/minute quota.
class FCMMessageQueue {
constructor(options = {}) {
this.messagesPerSecond = options.messagesPerSecond || 100; // Default: 100 msg/sec = 6K/min
this.maxQueueSize = options.maxQueueSize || 5000;
this.queue = [];
this.activeCount = 0;
this.lastSendTime = 0;
}
async enqueue(message) {
if (this.queue.length >= this.maxQueueSize) {
throw new Error('Message queue is full');
}
return new Promise((resolve, reject) => {
this.queue.push({ message, resolve, reject });
this.processQueue();
});
}
async processQueue() {
if (this.queue.length === 0 || this.activeCount >= this.messagesPerSecond) {
return;
}
const now = Date.now();
const timeSinceLastSend = now - this.lastSendTime;
const minInterval = 1000 / this.messagesPerSecond;
if (timeSinceLastSend < minInterval) {
const delayNeeded = minInterval - timeSinceLastSend;
setTimeout(() => this.processQueue(), delayNeeded);
return;
}
const { message, resolve, reject } = this.queue.shift();
this.activeCount++;
this.lastSendTime = Date.now();
try {
const result = await admin.messaging().send(message);
resolve(result);
} catch (error) {
reject(error);
} finally {
this.activeCount--;
this.processQueue();
}
}
}
// Usage
const messageQueue = new FCMMessageQueue({ messagesPerSecond: 100 });
// Queue 1000 messages - they will be sent at 100/second rate
for (const token of deviceTokens) {
await messageQueue.enqueue({
notification: { title: 'Update', body: 'New content available' },
token
});
}This ensures you stay well below Firebase's quotas while processing messages smoothly.
Check your actual quota consumption and limits to ensure you're operating below maximums.
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
2. Select your Firebase project
3. Navigate to APIs & Services → Library
4. Search for and select Firebase Cloud Messaging API
5. Click on the Quotas tab
6. Look for quotas like:
- "Requests per minute per project"
- "Messages per minute per topic" (if using topics)
- "Messages per minute per device" (if direct messaging)
Establish alerts if your usage approaches 80% of limits to get early warning of scaling issues.
Instead of sending individual messages for each event, batch multiple updates together.
class MessageBatcher {
constructor(options = {}) {
this.batchInterval = options.batchInterval || 5000; // Batch every 5 seconds
this.maxBatchSize = options.maxBatchSize || 100;
this.pendingMessages = [];
this.timer = null;
}
addMessage(message) {
this.pendingMessages.push(message);
if (this.pendingMessages.length >= this.maxBatchSize) {
this.flush();
} else if (!this.timer) {
this.timer = setTimeout(() => this.flush(), this.batchInterval);
}
}
async flush() {
if (this.pendingMessages.length === 0) return;
const batch = this.pendingMessages.splice(0);
this.timer = null;
// Send all messages in batch
const results = await admin.messaging().sendMulticast({
tokens: batch.map(m => m.token),
data: {
updates: JSON.stringify(batch.map(m => m.data)),
batchTime: new Date().toISOString()
},
notification: {
title: 'Updates Available',
body: `${batch.length} updates`
}
});
console.log(`Sent ${results.successCount} messages in batch`);
}
}
// Usage: Instead of 1000 messages, send 1 message with 1000 tokens
const batcher = new MessageBatcher();
for (const token of deviceTokens) {
batcher.addMessage({ token, data: { userId: '123' } });
}Batching reduces the number of FCM API calls significantly.
When sending the same message to multiple devices, use sendMulticast instead of multiple send calls.
// SLOW: 1000 separate API calls
const tokens = ['token1', 'token2', 'token3', ...]; // 1000 tokens
for (const token of tokens) {
await admin.messaging().send({
token,
notification: { title: 'Update', body: 'Available' }
});
}
// FAST: Fewer API calls, handles batching automatically
const message = {
notification: { title: 'Update', body: 'Available' }
};
// Process tokens in batches of 500 (FCM limit per call)
const batches = [];
for (let i = 0; i < tokens.length; i += 500) {
const batchTokens = tokens.slice(i, i + 500);
batches.push(
admin.messaging().sendMulticast({
tokens: batchTokens,
notification: message
})
);
}
const results = await Promise.all(batches);
const totalSuccess = results.reduce((sum, r) => sum + r.successCount, 0);
console.log(`Sent successfully to ${totalSuccess} devices`);sendMulticast is far more efficient and consumes fewer quota tokens than individual sends.
If you legitimately need to send more than 600K messages per minute, request a quota increase.
In Google Cloud Console:
1. Go to APIs & Services → Quotas
2. Search for "Firebase Cloud Messaging API"
3. Select the quota you need to increase
4. Click Edit Quotas
5. Provide details about your use case:
- Expected messages per minute
- Whether this is temporary (event-based) or permanent
- Your business justification
6. Submit the request
Important timing: Firebase recommends submitting quota increase requests at least 15 days in advance (30 days for requests exceeding 18M messages/minute) to allow time for approval and provisioning.
Note: Firebase Cloud Messaging itself is free - quota increases don't have a direct cost, but they do allocate backend resources to your project.
Understanding Quota Tokens and Token Buckets
Firebase uses a "token bucket" algorithm for rate limiting. Each 1-minute window, 600,000 tokens become available. Each FCM HTTP v1 API call consumes 1 token. When your bucket is empty, requests fail with 429 status.
The bucket is a sliding window - at the end of each minute mark, unused tokens are lost and the counter resets. Uneven send patterns can cause bursts to exceed the per-minute limit even if your overall rate is below average.
Per-Device and Per-Topic Limits
Beyond the project-wide 600K/minute limit, FCM has additional rate limits:
- Android per-device: 240 messages/minute and 5,000 messages/hour per device
- iOS per-device: Limited by Apple Push Notification (APNs) service limits
- Collapsible messages: Burst of 20 per device, then 1 message per 3 minutes
- Topic fanout: Up to 1,000 concurrent fanout operations per project
These are separate limits from the project quota. A single high-traffic device or massive topic fanout can hit these limits even if the project-wide quota hasn't been exceeded.
Production Scaling Patterns
For applications expecting millions of messages, implement a dedicated message queue (Cloud Tasks, Cloud Pub/Sub, or Redis) between your application and FCM:
const { CloudTasksClient } = require('@google-cloud/tasks');
const client = new CloudTasksClient();
async function enqueueFCMSend(message, delaySeconds = 0) {
const task = {
httpRequest: {
httpMethod: 'POST',
url: 'https://your-backend.com/internal/send-fcm',
headers: { 'Content-Type': 'application/json' },
body: Buffer.from(JSON.stringify(message)).toString('base64'),
},
scheduleTime: {
seconds: Math.floor(Date.now() / 1000) + delaySeconds
}
};
const parent = client.queuePath(projectId, 'us-central1', 'fcm-queue');
return client.createTask({ parent, task });
}This allows you to control send rate precisely, implement sophisticated retry logic, and gracefully handle backpressure.
Monitoring and Alerting
Set up Cloud Monitoring to track:
- FCM request rate (messages/second)
- 429 error rate and frequency
- Topic fanout completion time
- Per-device message frequency
Early warnings at 70-80% of quota help prevent user-facing outages.
messaging/UNSPECIFIED_ERROR: No additional information available
How to fix "messaging/UNSPECIFIED_ERROR: No additional information available" in Firebase Cloud Messaging
App Check: reCAPTCHA Score Too Low
App Check reCAPTCHA Score Too Low
storage/invalid-url: Invalid URL format for Cloud Storage reference
How to fix invalid URL format in Firebase Cloud Storage
auth/missing-uid: User ID identifier required
How to fix "auth/missing-uid: User ID identifier required" in Firebase
auth/invalid-argument: Invalid parameter passed to method
How to fix "auth/invalid-argument: Invalid parameter passed to method" in Firebase