# How to notify 50 million users about a flash sale: pull, push and FCM topics

The sale starts at 12:00. Marketing wants all **50 million users** to know, and most of them have the app closed.

Your server can't call a phone, and 50 million is a lot of phones to reach in one minute.

%[https://www.youtube.com/watch?v=5A_tlEoz6t4]

---

## TL;DR

- Polling at 50M users is 2.5 million requests a second, and almost every answer is "no".
- Pushing to each phone yourself means 50M messages. FCM's default quota is 600,000 a minute, so that's about 83 minutes.
- An FCM topic lets you send one message and Google copies it to every subscribed phone.
- Send it with high priority and a short expiry, and get your servers ready for 12:01.

---

## Pull or push

**Pull wastes millions of requests a second, and push to each phone hits a quota.**

![Pull: every phone asks the server every 20 seconds and mostly hears no. Push: the server sends one message through FCM, which uses a connection the phone already has](https://raw.githubusercontent.com/param4/system-design-with-param/main/shorts/07-flash-sale-notifications/boards/01-pull-vs-push-final.png)
_Left: every phone asks every 20 seconds. Right: one message goes out through FCM._

With pull, the app asks "is the sale live?" every 20 seconds. Across 50M phones that's 50M ÷ 20 = 2.5 million requests a second, and you run a fleet of servers whose only job is to say "not yet". Slow it to every 5 minutes and it's still ~167k requests a second, but now people hear about the sale up to 5 minutes late, which in a flash sale means sold out. Every poll also wakes the phone's radio, and Android's own docs call that a battery waste and point you to FCM.

Push goes through FCM (Firebase Cloud Messaging, Google's push service) on Android or APNs (Apple Push Notification service) on iPhone. Each Android phone already keeps one open connection to Google that all its apps share, and FCM sends your message down that pipe. The phone does nothing until there's news.

That's cheap for the phone, but it moves the work to your server. Each app install has a token, the address FCM uses to find that one phone. Loop over 50M tokens and you're sending 50M messages, and FCM allows **600,000 messages a minute** per Firebase project (your app's account on Firebase) by default. 50M ÷ 600k ≈ 83 minutes.

The quota counts messages, not API calls, so packing 500 tokens into one request still counts as 500. Adding servers doesn't help either: go over the quota and FCM replies `429 RESOURCE_EXHAUSTED` (HTTP's "too many requests") until the next minute. And you still own the token list and its retries.

---

## FCM topics: send one, let Google copy it

**Every app subscribes to one topic. Your server sends one message to that topic, and FCM copies it to every subscriber.**

This is pub/sub (publish/subscribe): receivers subscribe to a topic, a sender publishes to it, and the system does the fan-out, meaning it delivers a copy to each subscriber. FCM has it built in.

```kotlin
// in the app, once (Android)
Firebase.messaging.subscribeToTopic("flash_sale")
```

```js
// on your server, sent at 10:30 (Firebase Admin SDK, Google's server library for FCM)
await admin.messaging().send({
  topic: "flash_sale",
  notification: { title: "Flash sale at 12:00", body: "Up to 70% off, till 2 PM." },
  android: { priority: "high", ttl: 3.5 * 60 * 60 * 1000 },   // until 2 PM, counted from 10:30
  apns: { headers: { "apns-priority": "10", "apns-expiration": "<unix time of sale end>" } }, // 10 = deliver now
});
```

![Your server publishes one message to the flash_sale topic and FCM fans it out to 50 million subscribed phones](https://raw.githubusercontent.com/param4/system-design-with-param/main/shorts/07-flash-sale-notifications/boards/02-per-token-vs-topic-final.png)
_One call from your server. FCM copies it to every subscribed phone._

A topic has no subscriber limit, and you stop managing tokens and retries yourself.

Topics are not faster. Google says topic sends are tuned for throughput rather than speed, and [a fan-out rate of about 10,000 QPS](https://firebase.google.com/docs/cloud-messaging/topic-messaging) (phones reached per second) per project is common, but Google doesn't promise it and you can't raise it. At that rate 50M phones still takes ~83 minutes.

I still call topics the right answer, because the slow part moves to Google. You make one API call instead of running an 83-minute job that has to handle quotas and 429s on its own. For a broadcast that's the whole win. The catch is timing: start the send around 10:30 with "Flash sale at 12:00" in the text, so the last phones have it before the sale opens.

Two limits to know. Everyone gets the same text, so "Rahul, the shoes in your cart are 40% off" still needs per-token sends. And two topic sends running together share that capacity, so each goes at about half speed.

| Way | Load on you | Time for 50M | Personal message? |
|---|---|---|---|
| Polling every 20 s | 2.5M req/s, all day | up to 20 s late | Yes |
| Push to each token | 50M messages, quota-bound | ~83 min at 600k/min | Yes |
| FCM topic | 1 API call | ~83 min at ~10k/s, not guaranteed | No |

---

## Priority and expiry

**Two fields in that message decide whether the alert arrives on time and stops when the sale ends.**

A normal-priority message can wait while the phone sleeps in Doze (Android's battery-saving mode). High priority wakes it and delivers now. FCM may lower your priority if high-priority messages don't show a visible notification, but a sale alert does, so you're fine.

TTL (time to live) is how long FCM keeps trying a phone that's offline. The default is 4 weeks. A "sale is live" alert three days after the sale is worse than no alert, so set the TTL to expire when the sale ends. On iOS the same thing is the `apns-expiration` header.

Some people will never get it, because their phone is off or notifications are disabled. Plan for most users within about 90 minutes.

---

## The 12:01 rush

**The notification works. Then millions of people tap it in the same minute.**

![The notification lands on millions of phones, everyone taps at 12:01, and the traffic spike hits your servers; warmed-up servers absorb it](https://raw.githubusercontent.com/param4/system-design-with-param/main/shorts/07-flash-sale-notifications/boards/03-the-12-01-rush-final.png)
_Everyone taps at 12:01, and the spike hits your login and product pages together._

Autoscaling (adding servers automatically when load goes up) reacts to load that has already arrived, and new servers take minutes to boot. By then the first wave has already hit errors and left. Your servers need to be warmed up before 12:00, meaning already running, with caches filled and connections open. How to do that properly is a topic of its own.

---

## Interview follow-ups

- **"Only users in Mumbai should get it."** Subscribe each app to a city topic too, and send to a condition such as `'flash_sale' in topics && 'mumbai' in topics`. FCM allows up to five topics in one condition.
- **"Send everyone a personal offer."** Topics can't, so go back to per-token sends. Put them on a queue and pace them under the 600k/min quota, starting about 90 minutes before the sale.
- **"A user turned notifications off."** Push can't reach them. Show an in-app banner when they open the app, or fall back to email or SMS for the big sales.
- **"How do you know it was delivered?"** FCM's delivery reports in the Firebase console, or its export to BigQuery (Google's data warehouse) for your own queries. Then compare with how many people actually opened the app.

---

For a broadcast I'd use a topic: the 83 minutes become Google's job, not yours.

This is Day 6 of my system design shorts. [Subscribe on YouTube](https://www.youtube.com/@SystemDesignWithParam) or follow this blog for Day 7.

## References

- [Firebase: FCM throttling and quotas](https://firebase.google.com/docs/cloud-messaging/throttling-and-quotas)
- [Firebase: Topic messaging quotas and limits](https://firebase.google.com/docs/cloud-messaging/topic-messaging)
- [Firebase: Topic messaging on Android](https://firebase.google.com/docs/cloud-messaging/android/topic-messaging)
- [Firebase: Send with the Admin SDK](https://firebase.google.com/docs/cloud-messaging/send/admin-sdk)
- [Firebase: Message priority on Android](https://firebase.google.com/docs/cloud-messaging/android/message-priority)
- [Firebase: Setting message lifespan](https://firebase.google.com/docs/cloud-messaging/customize-messages/setting-message-lifespan)
- [Firebase: Understanding message delivery](https://firebase.google.com/docs/cloud-messaging/understand-delivery)
- [Android: Optimize server-initiated traffic](https://developer.android.com/topic/performance/power/network/action-server-traffic)

