How to fix messaging/topics-message-rate-exceeded: Topic subscribers receiving excessive messages in Firebase

FirebaseINTERMEDIATEMEDIUMUpdated Dec 22, 2025ยท 208 views

This error occurs when you exceed Firebase Cloud Messaging rate limits for sending messages to topics. Firebase restricts concurrent message fanouts to prevent abuse and ensure fair usage across all projects.

Quick fix

Fixes messaging/topics-message-rate-exceeded: Topic subscribers receiving excessive messages

const admin = require('firebase-admin');

async function sendToTopicWithRetry(message, topic, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await admin.messaging().sendToTopic(topic, message);
      console.log('Successfully sent message:', response);
      return response;
    } catch (error) {
      if (error.code === 'messaging/topics-message-rate-exceeded') {
        const waitTime = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
        console.log(`Rate limit hit, waiting ${waitTime}ms before retry...`);
        await new Promise(resolve => setTimeout(resolve, waitTime));
      } else {
        throw error; // Re-throw non-rate-limit errors
      }
    }
  }
  throw new Error('Max retries exceeded for topic message');
}
  1. Implement exponential backoff for retries
  2. Add rate limiting to your message sending logic
  3. Batch messages and reduce send frequency
  4. Use device groups or tokens for small audiences
firebase โ€” error
const admin = require('firebase-admin');
async function sendToTopicWithRetry(message, topic, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await admin.messaging().sendToTopic(topic, message);
console.log('Successfully sent message:', response);
return response;
} catch (error) {
if (error.code === 'messaging/topics-message-rate-exceeded') {
const waitTime = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
console.log(`Rate limit hit, waiting ${waitTime}ms before retry...`);
await new Promise(resolve => setTimeout(resolve, waitTime));
} else {
throw error; // Re-throw non-rate-limit errors
}
}
}
throw new Error('Max retries exceeded for topic message');
}
messaging/topics-message-rate-exceeded: Topic subscribers receiving excessive messages

What this error means

The `messaging/topics-message-rate-exceeded` error indicates that your application has exceeded Firebase Cloud Messaging (FCM) rate limits for topic-based messaging. Firebase imposes these limits to ensure fair resource allocation and prevent abuse of the messaging infrastructure. When you send messages to topics in FCM, the system performs "message fanout" - distributing a single message to potentially thousands or millions of subscribers. This process isn't instantaneous and requires significant backend resources. Firebase limits the number of concurrent message fanouts per project to 1,000. When you exceed this limit, additional fanout requests are either rejected with this error or deferred until in-progress fanouts complete. The error typically manifests as an HTTP 429 RESOURCE_EXHAUSTED response with the error code "QUOTA_EXCEEDED". The quota automatically refills in the following minute, but repeated violations can indicate architectural issues with your messaging strategy that need to be addressed.

Typical symptoms

  • HTTP 429 status code returned when sending messages to topics
  • Error code "messaging/topics-message-rate-exceeded" in server logs
  • Messages to topic subscribers fail to send during high-traffic periods
  • Intermittent notification delivery failures that resolve after waiting
  • Multiple concurrent topic sends failing simultaneously

Common causes

  1. Sending too many topic messages concurrently (exceeding 1,000 concurrent fanouts)
  2. Burst traffic patterns where many messages are sent in quick succession
  3. Large topic subscriber base causing slow fanout that overlaps with new sends
  4. Multiple services or processes sending to the same topics simultaneously
  5. Inefficient messaging strategy using topics instead of device groups for smaller audiences
  6. Not implementing proper rate limiting or backoff in application code

How to fix "messaging/topics-message-rate-exceeded: Topic subscribers receiving excessive messages"

1Implement exponential backoff for retries

When you receive a 429 error, implement exponential backoff to retry the request after waiting progressively longer periods.

javascript
const admin = require('firebase-admin');

async function sendToTopicWithRetry(message, topic, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await admin.messaging().sendToTopic(topic, message);
      console.log('Successfully sent message:', response);
      return response;
    } catch (error) {
      if (error.code === 'messaging/topics-message-rate-exceeded') {
        const waitTime = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
        console.log(`Rate limit hit, waiting ${waitTime}ms before retry...`);
        await new Promise(resolve => setTimeout(resolve, waitTime));
      } else {
        throw error; // Re-throw non-rate-limit errors
      }
    }
  }
  throw new Error('Max retries exceeded for topic message');
}

This ensures temporary rate limit issues don't cause permanent message failures.

2Add rate limiting to your message sending logic

Implement client-side rate limiting to prevent exceeding Firebase's concurrent fanout limits.

javascript
class TopicMessageQueue {
  constructor(maxConcurrent = 100) {
    this.maxConcurrent = maxConcurrent;
    this.activeRequests = 0;
    this.queue = [];
  }

  async send(topic, message) {
    // Wait if at capacity
    while (this.activeRequests >= this.maxConcurrent) {
      await new Promise(resolve => setTimeout(resolve, 100));
    }

    this.activeRequests++;
    try {
      const result = await admin.messaging().sendToTopic(topic, message);
      return result;
    } finally {
      this.activeRequests--;
      this.processQueue();
    }
  }

  processQueue() {
    if (this.queue.length > 0 && this.activeRequests < this.maxConcurrent) {
      const next = this.queue.shift();
      next();
    }
  }
}

// Usage
const messageQueue = new TopicMessageQueue(100);
await messageQueue.send('news-updates', {
  notification: { title: 'Breaking News', body: 'Story text' }
});

Keep your concurrent sends well below 1,000 to leave headroom for other processes.

3Batch messages and reduce send frequency

Instead of sending individual messages for every event, batch multiple updates together.

javascript
class MessageBatcher {
  constructor(topic, batchInterval = 5000) {
    this.topic = topic;
    this.batchInterval = batchInterval;
    this.pendingMessages = [];
    this.timer = null;
  }

  addMessage(data) {
    this.pendingMessages.push(data);

    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 single message with batched data
    await admin.messaging().sendToTopic(this.topic, {
      data: {
        updates: JSON.stringify(batch),
        count: batch.length.toString()
      }
    });
  }
}

// Usage: Send one message every 5 seconds instead of hundreds
const batcher = new MessageBatcher('stock-updates', 5000);
batcher.addMessage({ symbol: 'AAPL', price: 150.25 });
batcher.addMessage({ symbol: 'GOOGL', price: 2800.50 });

This dramatically reduces the number of topic sends.

4Use device groups or tokens for small audiences

Topics are designed for broadcasting to large audiences. For smaller, specific groups (under ~1,000 devices), use device group messaging or direct token sends.

javascript
// Instead of creating a topic for a small group
// BAD: Topic for 50 users
await admin.messaging().sendToTopic('team-alpha-notifications', message);

// GOOD: Direct send to device tokens
const deviceTokens = [
  'token1...', 'token2...', 'token3...' // Up to 500 tokens per batch
];

await admin.messaging().sendMulticast({
  tokens: deviceTokens,
  notification: {
    title: 'Team Update',
    body: 'New task assigned'
  }
});

This bypasses topic fanout limits entirely for targeted messaging.

5Monitor and optimize topic subscriber counts

Large topics with millions of subscribers take longer to fan out, increasing the chance of concurrent fanout limit issues.

Check your topic sizes and consider splitting very large topics:

javascript
// Split a massive global topic into regional topics
// Instead of: 'all-users' (5 million subscribers)
// Use: 'users-north-america', 'users-europe', 'users-asia'

async function sendToRegionalTopics(message) {
  const regions = ['north-america', 'europe', 'asia', 'latam'];

  for (const region of regions) {
    await sendToTopicWithRetry(message, `users-${region}`);
    // Add small delay between regions
    await new Promise(resolve => setTimeout(resolve, 500));
  }
}

Smaller topics complete fanout faster, reducing concurrent fanout count.

Advanced notes

Understanding Fanout Rate Limits

While Firebase limits concurrent fanouts to 1,000 per project, the actual achievable fanout rate can reach 10,000 QPS or higher depending on total system load. This means a single fanout can deliver messages to thousands of devices per second, but you can't have more than 1,000 of these operations running simultaneously.

Topic Subscription Limits

Be aware that each app instance can subscribe to a maximum of 2,000 topics, and the rate for adding/removing subscriptions is limited to 3,000 QPS per project. If you're also hitting subscription rate limits, you'll receive a 429 RESOURCE_EXHAUSTED error during subscribe/unsubscribe operations.

Production Architecture Patterns

For high-scale applications, consider implementing a message queue (Redis, RabbitMQ, Cloud Tasks) between your application and FCM. This allows you to control send rate precisely and handle backpressure gracefully:

javascript
// Using Cloud Tasks to throttle FCM sends
const { CloudTasksClient } = require('@google-cloud/tasks');
const client = new CloudTasksClient();

async function enqueueFCMSend(topic, message, delaySeconds = 0) {
  const task = {
    httpRequest: {
      httpMethod: 'POST',
      url: 'https://your-app.com/send-fcm',
      body: Buffer.from(JSON.stringify({ topic, message })).toString('base64'),
    },
    scheduleTime: {
      seconds: Date.now() / 1000 + delaySeconds
    }
  };

  await client.createTask({ parent: queuePath, task });
}

Monitoring and Alerting

Set up monitoring for FCM quota errors to detect patterns before they impact users. Track metrics like messages sent per minute, 429 error rate, and average fanout completion time.

Frequently asked questions

What does "messaging/topics-message-rate-exceeded: Topic subscribers receiving ex" mean?
The messaging/topics-message-rate-exceeded error indicates that your application has exceeded Firebase Cloud Messaging (FCM) rate limits for topic-based messaging. Firebase imposes these limits to ensure fair resource allocation and prevent abuse of the messaging infrastructure. When you send messages to topics in FCM, the system performs "message fanout" - distributing a single message to potentially thousands or millions of subscribers.
How do I fix "messaging/topics-message-rate-exceeded: Topic subscribers receiving ex"?
This error occurs when you exceed Firebase Cloud Messaging rate limits for sending messages to topics. Firebase restricts concurrent message fanouts to prevent abuse and ensure fair usage across all projects.
What causes "messaging/topics-message-rate-exceeded: Topic subscribers receiving ex"?
Sending too many topic messages concurrently (exceeding 1,000 concurrent fanouts) Burst traffic patterns where many messages are sent in quick succession Large topic subscriber base causing slow fanout that overlaps with new sends
Is the "messaging/topics-message-rate-exceeded: Topic subscribers receiving ex" error serious?
It is a medium-severity error โ€” more of a blocker than a risk. The steps above resolve it.

Official resources & further reading