Back to Insights

Firebase · Published 2026-08-15 · 6 min read · Waris Labs

Scheduling Push Notifications at Scale with Firebase Cloud Functions

Design a scalable scheduled push notification system with Firebase Cloud Functions, Firestore queues, FCM, pagination, retries, and safe batch processing.

Published by Waris Labs, a software engineering studio publishing practical guides on React Native, Firebase, cloud systems, app publishing, and AI automation.

Waris Labs Insights are educational technical notes. Verify platform requirements, security impact, and production behavior before applying code or configuration changes.

Sending a push notification to ten users is easy. Sending scheduled notifications to tens of thousands of users without timeouts, duplicate sends, memory spikes, or quota surprises requires architecture.

The common mistake is writing one scheduled function that loads every user, loops through the entire list, and sends messages until it finishes. That can work during testing, then fail when the app grows. A scalable design treats notifications as jobs, not as one giant loop.

Key Takeaways

  • Scheduled push delivery should be modeled as durable jobs, not as one unbounded user loop.
  • Each function invocation should claim a limited batch, process it safely, and leave remaining jobs for later runs.
  • FCM payloads should stay small and avoid sensitive private data.
  • Retry logic needs both backoff and terminal states so failures do not repeat forever.
  • Batch sizes, shard counts, and schedules are engineering recommendations that should be tuned from real workload data.

The Core Architecture

A practical Firebase architecture has four parts:

  • Firestore stores notification jobs or queue documents.
  • Cloud Scheduler triggers a Cloud Function on an interval.
  • The function claims a small batch of due jobs.
  • Firebase Cloud Messaging sends the messages.

The queue might look like this:

notificationJobs/{jobId}

Each document can include:

{
  "uid": "user_123",
  "sendAt": "2026-08-15T07:00:00.000Z",
  "status": "pending",
  "type": "daily-workout-reminder",
  "attempts": 0,
  "lastError": null
}

For purely local reminders, you may not need a backend at all. See How Local Notifications Work in React Native When the App Is Closed for device-scheduled reminders.

Why Use a Queue

A queue gives the system a durable source of truth. If a function times out, the job still exists. If FCM rejects one token, the rest of the batch can continue. If the app grows, you can tune batch size without rewriting product logic.

Avoid treating a scheduled function as the only state. The function should process a known set of due work, update status, and exit cleanly.

Scheduled Functions

Firebase supports scheduled functions that run on a cron-style schedule. The official guide is here: Schedule functions.

A simple schedule might run every minute or every five minutes, depending on the precision your product needs. For many reminder products, minute-perfect delivery is less important than reliability and predictable load.

import { onSchedule } from 'firebase-functions/v2/scheduler';

export const processNotificationQueue = onSchedule('every 5 minutes', async () => {
  // Claim and process a limited batch of due jobs.
});

Do not start by running every few seconds. That creates operational noise and usually does not improve the user experience.

Pagination and Batch Processing

Read a limited number of pending jobs:

const dueJobsQuery = db
  .collection('notificationJobs')
  .where('status', '==', 'pending')
  .where('sendAt', '<=', new Date())
  .orderBy('sendAt', 'asc')
  .limit(250);

Process the batch, update statuses, and let the next scheduled run pick up more. If your system needs higher throughput, shard by time bucket or status partition instead of loading everything into memory.

The limit(250) value above is a practical example, not an official Firebase requirement. Start with a small batch that finishes comfortably inside your timeout, then tune it with production metrics.

For example:

notificationBuckets/2026-08-15-07-00/jobs/{jobId}

This can make it easier to process one due bucket at a time, especially when a product sends reminders at common times such as 7 AM local time.

Avoid Loading Thousands of Users at Once

A common design is:

const users = await db.collection('users').get();

That is a red flag for scheduled notification systems. It reads every user whether they need a notification or not. It also grows slower and more expensive as the user base grows.

Instead, create queue jobs when the user configures reminders, or maintain a queryable reminder schedule collection with narrow due windows. The scheduled function should ask "what is due now?" not "which users exist?"

Sending with FCM

Firebase Cloud Messaging can send messages through server-side code using the Admin SDK. The official Admin SDK send guide is here: Send a message using Firebase Admin SDK.

Keep payloads small and intentional:

const message = {
  token: user.fcmToken,
  notification: {
    title: 'Workout reminder',
    body: 'Your planned session is ready.',
  },
  data: {
    screen: 'WorkoutPlan',
    reminderId: job.id,
  },
};

Use notification fields when you want the platform to display a notification. Use data fields for routing and app-specific context. Do not put sensitive private data in notification payloads because lock screens, logs, and third-party services may expose more than you expect.

Retry Strategy

Every job should have an attempt count and a terminal state. Retry transient failures, but do not retry forever.

Possible statuses:

  • pending
  • processing
  • sent
  • retry
  • failed
  • cancelled

Use exponential backoff for temporary errors:

function nextRetryAt(attempts: number) {
  const minutes = Math.min(60, 2 ** attempts);
  return new Date(Date.now() + minutes * 60 * 1000);
}

When FCM reports a permanently invalid token, mark the token inactive and stop sending to it. Invalid tokens are not a reason to retry the same job indefinitely.

Function Timeouts and Memory Limits

Cloud Functions have execution limits. Even when configured generously, a notification job should finish comfortably inside the timeout. Long-running functions are harder to observe and easier to duplicate.

Keep each run small:

  • Claim a limited batch.
  • Send messages in controlled chunks.
  • Update job status after each chunk.
  • Stop before timeout risk.
  • Let the next scheduled run continue.

If your function sometimes processes 250 jobs and sometimes 25,000 jobs, the batch boundary is wrong.

Claiming Work Safely

Two function instances can overlap if one run is slow or if a retry happens. Avoid double sends by claiming jobs before sending. A transaction or status update can move jobs from pending to processing with a lease timestamp.

await jobRef.update({
  status: 'processing',
  processingStartedAt: new Date(),
  leaseExpiresAt: new Date(Date.now() + 10 * 60 * 1000),
});

If a function crashes after claiming a job, a later run can return expired leases to pending or retry.

Common Payload Mistakes

Teams often send too much data. A notification should not carry a full workout plan, invoice, medical note, or private message body unless the product has explicitly decided that lock-screen exposure is acceptable.

Other common mistakes:

  • Missing Android notification channel
  • Using one token forever without refreshing it
  • Not handling unregistered tokens
  • Sending duplicate jobs after preference changes
  • Ignoring user timezone
  • Scheduling based on server timezone instead of user intent

For a fitness app data model that keeps notification preferences user-owned, see Firebase Firestore Architecture for a React Native Fitness App.

Scaling From Small Apps

For a small app, one scheduled function and one queue collection may be enough. As the app grows, split work by shard, region, due-time bucket, or notification type. Add metrics before rewriting:

  • Jobs due per interval
  • Jobs sent per run
  • Average function duration
  • FCM failure rate
  • Invalid token count
  • Retry count
  • Duplicate prevention count

Scaling should follow evidence, not anxiety.

Official Documentation

Firebase documents the supported scheduling, querying, messaging, timeout, and memory configuration APIs. Queue design, batch sizing, retry windows, and observability choices are Waris Labs recommendations for production-oriented notification systems.

Conclusion

Scheduled push notifications at scale need a durable queue, narrow queries, bounded function work, careful FCM payloads, and a retry model. Avoid loading every user, avoid unbounded loops, and treat every notification as a job with state. That architecture works for small products and gives you a path to tens of thousands of users without turning reminder delivery into a fragile background script.

Related Articles