How does a notification system deliver push notifications to millions of devices reliably?
How does a notification system deliver push notifications to millions of devices without duplicates, handling device token lifecycle, retry logic, and cross-platform delivery (iOS/Android) reliably?
What the system must do · reliable cross-platform push delivery with deduplication
Quality constraints for reliable cross-platform push delivery at billion-device scale
| Property | Target | Why It Matters / Design Impact |
|---|---|---|
| Delivery latency | P95 <2s from trigger to device display | Notifications must arrive while context is fresh · delayed push loses relevance. |
| Throughput | 1B+ notifications/day (~12K/sec avg, 50K/sec peak) | Must sustain high throughput without backpressure on message delivery pipeline. |
| Deduplication | Zero duplicate notifications (idempotency key) | Duplicate buzzes annoy users and erode trust. Idempotency key per push event. |
| Reliability | At-least-once trigger, at-most-once display | Never miss a trigger event, but never show the same notification twice on device. |
| Token freshness | Handle 10M+ token rotations/day | Stale tokens cause silent delivery failures. Must detect and refresh proactively. |
| Platform coverage | iOS (APNs) + Android (FCM) + Web (WebPush) | Each platform has different protocols, rate limits, and error semantics. |
| Batching | Collapse multiple msgs in same chat into single notification | Prevents notification spam when user receives burst of messages in one conversation. |
Deriving push volume from message patterns and device registry size
| Step | Calculation | Result |
|---|---|---|
| 1 | 2B users · 50% with push enabled | 1B push-eligible devices |
| 2 | 100B msgs/day · 30% to offline users | 30B offline msgs → triggers |
| 3 | Collapse/batch: 30B triggers · 30 (avg batch) | ~1B actual pushes/day |
| 4 | 1B · 86400 | ~12K pushes/sec avg, peak 50K/sec |
| 5 | Device registry: 1B devices · 200 bytes | ~200GB (Cassandra) |
| 6 | Dedup store: 1B keys/day · 32 bytes · 24h TTL | ~32GB Redis |
Event trigger → Notification Service → Platform Router → APNs/FCM delivery with retry and dedup
Reliability and deduplication are the primary concerns · not speed
| Decision | Choice | Why | Alternative Rejected |
|---|---|---|---|
| Deduplication | Idempotency key per notification | hash(user_id + message_id) stored in Redis with 24h TTL. Prevents duplicate pushes on retry. | No dedup: users receive same notification 2-3· on transient failures. Unacceptable UX. |
| Token Storage | Device Registry (Cassandra) | user_id → list of {device_id, token, platform, last_seen}. Supports multi-device. Handles token rotation. | Single token per user: breaks multi-device. In-memory: lost on restart. |
| Platform Routing | Separate sender per platform | APNs and FCM have different APIs, rate limits, and error codes. Separate senders allow independent scaling. | Unified sender: complex error handling, can't scale platforms independently. |
| Trigger Mechanism | Kafka consumer on offline-delivery topic | Same event that triggers offline message storage also triggers push. Decoupled from message delivery path. | Synchronous push on message send: adds latency to sender, couples systems. |
| Collapse Strategy | Collapse key = conversation_id | Multiple messages in same chat → single notification updated. Reduces notification fatigue. | One push per message: 50 messages = 50 notifications. Terrible UX in active group chats. |
Push delivery is inherently unreliable · the system must handle every failure mode gracefully
| # | Failure / Edge Case | What Breaks | How It's Handled |
|---|---|---|---|
| 1 | APNs returns "invalid token" (410) | Token is stale · app was uninstalled or token rotated | Mark token as dead in registry. Do not retry. Wait for app to re-register with new token on next launch. |
| 2 | FCM rate limit (429) | Too many requests to FCM · pushes delayed | Exponential backoff per FCM's Retry-After header. Queue notifications in Kafka · no data loss. |
| 3 | User has 3 devices, message sent to all | User reads on one device, still gets push on others | On message read (any device), cancel pending pushes for that message via collapse key update. |
| 4 | Notification service crashes mid-batch | Some notifications sent, some not · risk of duplicates on restart | Idempotency key check before every send. On restart, re-process Kafka offset · dupes are caught by Redis dedup set. |
| 5 | Badge count drift | App icon shows wrong unread count | Badge count is server-authoritative. On each push, include absolute badge count (not increment). App syncs on foreground. |
Optimized for reliability and deduplication over raw speed
| Component | Technology | Why This | Why Not X |
|---|---|---|---|
| Event Source | Kafka (offline-delivery topic) | Same event triggers offline storage + push. Decoupled, replayable. Consumer group for push senders. | Direct call from message service: couples systems, no replay on failure. |
| Device Registry | Cassandra | High write throughput for token updates. Partition by user_id. Multi-device support. No single point of failure. | MySQL: write bottleneck at scale. Redis: not durable enough for token registry. |
| Dedup Store | Redis (SET with TTL) | O(1) idempotency check. 24h TTL auto-cleans. Handles retry storms without duplicates. | Cassandra: too slow for per-notification check. In-memory: lost on restart. |
| APNs Client | HTTP/2 persistent connection | Apple requires HTTP/2. Persistent connection avoids TLS handshake per push. Multiplexed streams. | Legacy binary protocol: deprecated. New connection per push: 100ms+ TLS overhead. |
| FCM Client | HTTP/2 + batch API | Send up to 500 notifications per batch request. Reduces HTTP overhead by 100·. | Individual sends: 1 HTTP request per notification · doesn't scale to millions/day. |
The 5 things an interviewer wants to hear about push notifications at scale