How push notifications reach your phone
How APNs and FCM maintain persistent connections, handle token management, topic-based routing, and delivery confirmation for billions of daily notifications.
The Problem Statement
Interviewer: "You send a push notification from your backend to a user's phone. The user might be on WiFi, on cellular, or have their phone off. Walk me through exactly what happens from the moment your server sends the notification to the moment the user sees it on their lock screen."
This question tests three things: whether you understand the role of platform intermediaries (APNs for Apple, FCM for Google), how persistent connections from billions of devices are managed without overwhelming infrastructure, and whether you can reason about delivery guarantees when devices go offline and come back.
I see candidates stumble on this one because they think their app server sends the notification directly to the phone. It does not. The entire system depends on a persistent connection that the phone's operating system maintains to a platform relay (Apple or Google), and your server talks to that relay, not to the phone itself.
Clarifying the Scenario
You: "Before I walk through the full path, let me clarify scope."
You: "Should I cover both iOS (APNs) and Android (FCM), or focus on one platform?"
Interviewer: "Cover the general architecture, then call out differences where they matter."
You: "Got it. And are we talking about a single notification to one device, or a broadcast to millions of users?"
Interviewer: "Start with the single-device case, then I want to hear about broadcast and topic-based routing."
You: "Should I also cover silent notifications, the ones that wake up the app to sync data without showing anything to the user?"
Interviewer: "Yes, briefly. I want to see that you know the difference."
You: "OK. I will structure my answer in three layers: the persistent connection from the device to the platform (APNs/FCM), the delivery pipeline from your server through the platform to the device, and the edge cases around offline delivery, token management, and prioritization."
My Approach
I break this into five parts:
- The persistent connection: Every phone maintains a single long-lived TCP/TLS connection to the platform push service. This is managed by the OS, not by your app.
- Device token registration: When your app installs, it registers with the platform and gets a unique device token. This token is what your server uses to address a specific device.
- The delivery pipeline: Your server sends a notification payload to the platform API (APNs or FCM), the platform routes it over the persistent connection to the device, and the OS delivers it to your app.
- Topic-based routing and broadcast: For sending to millions of users, platforms support topic subscriptions so you send once and the platform fans out.
- Offline delivery and failure handling: What happens when the device is off, the token is invalid, or the notification is too old to matter.
Let me walk through the history briefly, because it explains why the system is designed this way. Before APNs existed (pre-2009), apps had two options: keep a background socket open (battery killer) or poll a server periodically (latency killer). Neither worked well. Apple designed APNs as a shared infrastructure: one connection per device, managed by the OS, efficient for both battery and latency. Google followed with C2DM (later GCM, now FCM) for the same reason.
The brilliance of the design is that it solves the "notification problem" for every app on the device with a single persistent connection. The app developer does not manage connections, heartbeats, or reconnection. You just send a payload to an API, and the platform handles delivery.
The cost: you are entirely dependent on Apple and Google infrastructure. If APNs goes down, no iOS notifications work, period. There is no fallback. This dependency is the fundamental tradeoff of the push notification architecture.
The Architecture
Before the diagram, let me set the mental model. The entire push notification system has three actors, and most candidates only think about two of them.
Actor 1: Your server (the sender). Actor 2: The phone (the receiver). Actor 3: The platform push service (APNs or FCM), which is the invisible relay between the other two. Your server never talks to the phone. The platform is always in the middle.
This three-actor model is not optional. Apple and Google designed it this way for two reasons: battery efficiency (one shared connection per device instead of one per app) and security (the platform authenticates both sides and prevents spam).
Here is the full path. Your notification service builds a payload (title, body, badge count, custom data) and puts it into a send queue. The queue handles rate limiting (APNs allows bursts but can throttle) and retry logic.
The queue sends the notification over HTTP/2 with TLS to the platform push service. For iOS users, it goes to APNs. For Android, it goes to FCM. Each request includes the device token (a unique identifier for that device-app combination) and the notification payload.
The platform push service looks up which persistent connection belongs to that device token and routes the notification over it. The device's OS-level push daemon receives it, identifies which app it belongs to, and either shows it on the lock screen (if the app is backgrounded) or delivers it to the app's notification handler (if the app is in the foreground).
The critical insight: your server never talks to the phone directly. The platform is always the intermediary. This is why push notifications work even when the phone changes IP addresses, switches between WiFi and cellular, or moves between cell towers.
The payload itself has a strict format. For APNs, it is a JSON object with an aps key containing alert (title, body, subtitle), badge (the red number on the app icon), sound, and content-available (for silent notifications). For FCM, you choose between a notification object (platform-handled display) or a data object (app-handled processing). The maximum payload size is 4KB for APNs and 4KB for FCM data messages.
Here is what a typical APNs payload looks like:
{
"aps": {
"alert": {
"title": "New message from Alice",
"body": "Hey, are you free for lunch?"
},
"badge": 3,
"sound": "default"
},
"conversation_id": "conv-42",
"message_id": "msg-789"
}
The aps key is required and platform-defined. Everything outside aps is custom data that your app receives when the user taps the notification. This is how your app knows which conversation to open.
For FCM, the equivalent data message looks similar but uses data as the top-level key, and the display is handled entirely by your app code.
Both APNs and FCM use a single persistent connection per device, not per app. If you have 30 apps installed with push notifications enabled, all 30 share one connection to APNs. This is how Apple and Google handle billions of devices without billions of separate connections per app.
The Persistent Connection Architecture
This is the piece most candidates do not understand. Every iPhone maintains a single, always-on TLS connection to APNs. Every Android phone does the same with FCM. This is what makes push notifications "push" instead of "poll."
The connection is established when the phone boots (or when the OS push service starts) and it stays alive indefinitely. The phone and platform exchange heartbeat pings every 15-25 minutes (the interval varies by network conditions and battery state) to keep the connection from being dropped by NAT middleboxes.
Why heartbeats? Because the phone is behind a NAT (Network Address Translation) device, either the home router or the cellular carrier's NAT. NAT devices track active connections in a table and drop entries that have been idle too long. Without heartbeats, the NAT drops the mapping after 5-30 minutes (carrier-dependent), and the persistent connection silently dies. The phone thinks it is still connected, but packets from APNs can no longer reach it. The heartbeat keeps the NAT entry alive.
When the phone switches networks (WiFi to cellular, or between cell towers), the old TCP connection dies. The phone detects this and establishes a new connection. Any notifications that were queued during the brief disconnection are delivered immediately on the new connection.
I want to emphasize something: this persistent connection is managed by the operating system, not by your app. Your app cannot open its own persistent connection to your server efficiently (the OS will kill background network connections to save battery). This is precisely why the platform intermediary exists.
Here is a number that puts this in perspective. Apple has over 1.5 billion active devices. Each device maintains one persistent connection to APNs. That is 1.5 billion concurrent TCP connections. Apple's push infrastructure is one of the largest persistent connection systems ever built, rivaling only Google's FCM fleet and WhatsApp's Erlang-based connection servers.
The heartbeat interval is also more nuanced than it first appears. On WiFi, the interval can be longer (up to 30 minutes) because NAT tables in home routers are generous. On cellular, NAT tables expire faster (some carriers kill idle connections after 5 minutes), so the heartbeat interval drops to 15 minutes or less. The OS adapts the interval based on the current network type to minimize both radio wakeups and connection drops.
A common interview mistake: saying "the app opens a WebSocket to our server for push notifications." On mobile, the OS kills background WebSocket connections to save battery. The only reliable way to reach a phone is through the platform push service (APNs/FCM). WebSockets work for web browsers, not for mobile push.
Token Management and Device Registration
The device token is the address your server uses to reach a specific device. Getting it right is surprisingly complex, because tokens change and expire.
When a user installs your app and grants notification permission, the app calls the OS registration API. The OS contacts the platform (APNs or FCM) and gets back a device token. This token is unique to the combination of device + app + environment (production vs sandbox).
Your app immediately sends this token to your backend, which stores it mapped to the user ID. When you want to send a notification to user Alice, you look up all her device tokens (she might have an iPhone and an iPad) and send to each one.
Here is where it gets tricky. Tokens are not permanent.
On iOS, APNs can rotate the device token at any time (typically on OS updates, app reinstalls, or restoring from backup). Your app must check for a new token on every launch and re-register if it changed. On Android, FCM tokens can also change, and the onTokenRefresh callback fires when this happens.
The silent killer is uninstalled apps. When a user uninstalls your app, the token becomes invalid, but nobody notifies your server. You find out only when you try to send a notification and the platform returns an error ("InvalidRegistration" from FCM, or the token appears in the APNs feedback service). Your server must handle these errors by removing the stale token.
For your interview: mention token lifecycle explicitly. It shows you have built real notification systems, not just read about them.
A subtle point about token scope: APNs tokens are specific to the app AND the environment (sandbox vs production). A token generated in the development sandbox is completely different from the production token for the same app on the same device. This catches many developers during their first production launch, because all their stored tokens are sandbox tokens that silently fail in production.
The data model for your token store matters too. A well-designed token table looks like this:
| Column | Purpose |
|---|---|
user_id | Which user this device belongs to |
device_token | The platform-specific token (primary key) |
platform | "ios" or "android" |
app_version | The version that registered the token |
created_at | When the token was first seen |
last_refreshed_at | Last time the app confirmed this token |
last_sent_at | Last time you sent a notification to this token |
failure_count | Number of consecutive platform errors |
is_active | Whether this token should receive notifications |
The last_refreshed_at column is the most important one for hygiene. If a token has not been refreshed in 30 days, the app has not launched in 30 days, which means the user is likely churned or uninstalled.
One more detail that catches developers in production: a single user can have multiple tokens. Alice has an iPhone and an iPad, both running your app. That is two tokens for user_id = "alice." When you send a notification for a new chat message, you must send to both tokens. If Alice reads the message on her iPhone, you might want to cancel or update the notification on her iPad. APNs does not support notification recall (once sent, it cannot be unsent). Your app's background handler must check whether the message has been read and dismiss the local notification if so.
Topic-Based Routing and Broadcast
When you need to send a notification to millions of users (a breaking news alert, a flash sale announcement, a system maintenance warning), sending individual API calls for each device token is slow and expensive. Both platforms solve this with topic-based subscriptions.
On the device side, the app subscribes to topics during registration: messaging.subscribeToTopic("news"). On the server side, you publish once to the topic, and the platform handles the fan-out to all subscribed devices. One API call reaches millions of devices.
FCM supports arbitrary string topics. APNs supports a similar concept through "channels" in iOS 16+. For older iOS versions, the workaround was maintaining your own subscriber lists and batching individual sends.
The tradeoff with topics: you lose per-user personalization. Every subscriber gets the same payload. If you need to say "Hey Alice, your order is ready" to millions of users with different names and orders, you still need individual sends. Topics are for broadcast content that is identical for everyone.
My recommendation: use topics for broadcast (marketing, news, system alerts) and individual sends for personalized content (messages, order updates, account activity). Most apps need both.
For your interview: mentioning topic-based fan-out shows you have thought about scale. "For broadcast to millions, I would use FCM topics so one API call triggers platform-managed fan-out, instead of sending millions of individual requests from my server."
Delivery Guarantees and Failure Handling
The third deep dive is what happens when the notification cannot be delivered immediately. The phone is off, the user is in airplane mode, or the network is congested.
When your server sends a notification, the platform makes a routing decision. If the device is currently connected, the notification goes through immediately (under 500ms end-to-end). If the device is offline, the notification enters a platform-managed queue.
The queue respects the time-to-live (TTL) value you set. A breaking news notification might have a 1-hour TTL (stale after that). A chat message might have a 28-day TTL (deliver whenever the user comes back). If the device does not reconnect before the TTL expires, the notification is silently discarded.
The collapse mechanism is important for chatty apps. If your app sends "3 new messages," then "4 new messages," then "5 new messages" while the phone is offline, you do not want the user to see all three. Setting the same collapse_id tells the platform to keep only the latest notification with that ID. The user sees "5 new messages" once, not three stacked notifications.
APNs calls this the apns-collapse-id header. FCM uses the collapse_key parameter. Both work the same way: only one notification per collapse key is stored in the queue. New notifications with the same key replace older ones.
The interaction between TTL, collapse, and priority creates a powerful configuration matrix. Here is how I think about it for different notification types:
| Notification Type | TTL | Collapse | Priority | Reasoning |
|---|---|---|---|---|
| Direct message | 28 days | Per-conversation | High | User must see it, collapse avoids flood |
| Like on a post | 1 day | Per-post | Normal | Nice to know, not urgent, one per post is enough |
| Ride arriving | 5 min | Per-ride | High | Stale after 5 min, must wake phone immediately |
| Marketing promo | 4 hours | Per-campaign | Normal | Time-boxed, one per campaign, battery-friendly |
| Security alert | 7 days | None | High | Every alert matters, no collapsing, immediate delivery |
The key differentiator in an interview: mention collapse_id and TTL together. It shows you have worked with the actual platform APIs, not just the concept. "I set collapse_id per conversation thread and TTL based on whether the notification is time-sensitive or persistent."
The Tricky Parts
-
Silent notifications for data sync: Both platforms support "silent" or "data-only" notifications that wake the app in the background without showing anything to the user. These are used for inbox sync, content prefetch, or triggering a local database update. The catch: both iOS and Android heavily throttle silent notifications (iOS limits to ~2 per hour), so you cannot use them as a general-purpose messaging channel.
-
Notification grouping on the device: Even with collapse_id, the OS groups notifications by app on the lock screen. iOS 15+ supports "notification summaries" that batch non-urgent notifications and deliver them at scheduled times. Your server has no control over this grouping once the notification reaches the device. This means your carefully designed notification copy might show up in a summary as "12 notifications from YourApp" instead of the individual titles you wrote.
-
End-to-end encryption complications: WhatsApp and Signal send encrypted message content in push notifications, but APNs/FCM can see the payload (it is encrypted in transit, but the platform can read it). To keep message content private, these apps send a silent notification that says "you have a new message," and the app decrypts the content locally when it wakes up. This adds latency to the user experience.
-
Multi-device scenarios: A user logged into your app on their iPhone, iPad, and MacBook should not receive the same notification three times for the same event. Your server must decide whether to send to all devices (for an alarm), the most recently active device (for a message), or only devices not currently active (for a missed call). This is a server-side deduplication problem that requires tracking device activity state.
-
Platform quotas and throttling: FCM has no hard per-message quota but throttles apps that send too many notifications to offline devices. APNs can reject connections from servers that repeatedly send to invalid tokens. Understanding these limits is essential for high-volume notification systems. FCM also has "high priority" quotas: if your app sends too many high-priority messages that the user does not interact with, the platform may downgrade future messages to normal priority.
A useful mental model: think of the platform push service as a post office. Your server drops off the letter (notification payload) addressed to a mailbox (device token). The post office (APNs/FCM) delivers it. If the mailbox does not exist anymore (uninstalled app), the letter gets returned. If the recipient is on vacation (device offline), the post office holds it for a while (TTL). If you keep sending letters to nonexistent mailboxes, the post office flags your account for abuse (throttling).
What Most People Get Wrong
| Mistake | What they say | Why it is wrong | What to say instead |
|---|---|---|---|
| Direct connection myth | "Our server sends the notification directly to the phone" | Your server talks to APNs/FCM, never to the phone. The platform maintains the persistent connection. | "Our server sends to the platform API (APNs/FCM), which routes it over the platform's persistent connection to the device." |
| Ignoring token lifecycle | "We store the token at registration and send to it forever" | Tokens rotate on OS updates, app reinstalls, and periodically. Stale tokens cause delivery failures and throttling. | "Tokens refresh on every app launch. We clean up invalid tokens on platform error responses and audit after 30 days of inactivity." |
| No TTL | "We just send the notification and hope it arrives" | Without TTL, a user turning on their phone after 3 days gets a flood of stale notifications. | "Every notification has a TTL based on its type. Time-sensitive alerts expire in hours, persistent messages last up to 28 days." |
| Confusing high/normal priority | "We set all notifications to high priority for fast delivery" | Platforms throttle apps that abuse high priority. High priority wakes the phone from power-saving mode and drains battery. | "High priority only for direct user actions (messages, calls). Marketing and digest notifications use normal priority." |
| Forgetting platform differences | "Push notifications work the same on iOS and Android" | APNs and FCM have different payload formats, size limits (4KB for APNs, 4KB for FCM data message), token formats, and delivery semantics. | "I handle iOS and Android as separate pipelines. Different payload formats, different token lifecycles, different priority semantics." |
Another trap: candidates sometimes say "we will use Firebase for push notifications" as if Firebase is the only option. Firebase Cloud Messaging (FCM) is Google's service for Android (and cross-platform). APNs is Apple's service for iOS. You must integrate with both if you support both platforms. Firebase can act as a unified abstraction layer that talks to APNs for iOS devices, but under the hood, the notification still goes through APNs. Knowing this distinction shows production experience.
How I Would Communicate This in an Interview
Here is how I would actually say this. I have practiced this enough that it flows in under 90 seconds.
"Push notifications depend on a persistent connection that the phone's operating system maintains to the platform push service, APNs for Apple and FCM for Google. This is a single TLS connection per device, shared by all apps. It stays open indefinitely with periodic heartbeats to keep NAT entries alive.
When your app installs and the user grants notification permission, the OS registers with the platform and gets a device token. Your app sends this token to your backend, which stores it mapped to the user ID.
To send a notification, your server makes an HTTP/2 request to the platform API with the device token and payload. The platform looks up which persistent connection belongs to that device and delivers the notification over it. The OS on the device shows it on the lock screen or hands it to your app if it is in the foreground.
The interesting edge cases are around offline delivery and token management. If the device is offline, the platform queues the notification until the device reconnects, subject to a TTL you set. I always set TTL based on notification type: 5 minutes for time-sensitive alerts like ride arrival, 7 days for persistent content like messages.
Tokens rotate periodically, so your app must re-register on every launch and your server must clean up invalid tokens when the platform returns errors. I track a last_refreshed_at timestamp per token and audit tokens that have not refreshed in 30 days.
For high-volume scenarios like broadcasting to millions of users, both platforms support topic-based subscriptions. You subscribe devices to topics on registration, and when you publish to a topic, the platform handles the fan-out. This is much more efficient than sending millions of individual API calls."
Notice the structure: connection model first (the foundation), then token registration (the setup), then the delivery path (the happy path), then edge cases (offline, tokens), then scale (topics). This layered approach shows the interviewer I can think systematically.
I would then offer: "Should I go deeper on the token management lifecycle, the priority system, or the offline delivery guarantees? Those are the three areas where production experience matters most."
Interview Cheat Sheet
- When asked about the connection model: "One persistent TLS connection per device to the platform (APNs/FCM). OS-managed, shared by all apps. Your server never connects directly to the phone."
- When asked about the delivery path: "Server β Platform API (HTTP/2) β Platform routes by token over persistent connection β Device OS β Your app."
- When asked about offline delivery: "Platform queues notifications with a TTL. When the device reconnects, queued notifications drain in order. Expired notifications are silently discarded."
- When asked about tokens: "Tokens are generated on app registration and can change on OS updates or reinstalls. Re-register on every app launch. Clean up invalid tokens on platform error responses."
- When asked about priority: "High priority for direct user actions (wakes phone from Doze mode). Normal priority for everything else (batched delivery, battery-friendly)."
- When asked about broadcast: "Topic-based subscriptions. Devices subscribe to topics, server publishes to a topic once, platform fans out to all subscribers."
- When asked about silent notifications: "Data-only notifications that wake the app to sync data without showing anything to the user. Heavily throttled by both platforms (iOS: ~2/hour)."
- When asked about collapse: "Set a collapse_id to replace older queued notifications with newer ones. 5 unread count notifications become 1."
- When asked about security: "The platform can read notification payloads. For E2E encrypted apps, send a silent notification to trigger local decryption rather than putting message content in the payload."
- When asked about multi-device: "Server-side logic decides which devices to target. Send to the most recently active device for messages, all devices for alarms, inactive devices for missed calls."
Test Your Understanding
Quick Recap
- Push notifications travel from your server to a platform intermediary (APNs for iOS, FCM for Android), which routes them over a persistent connection to the device, and your server never talks to the phone directly.
- The phone maintains a single OS-managed TLS connection to the platform, shared by all apps, with periodic heartbeats to keep NAT entries alive, which is why push works without destroying battery life.
- Device tokens are the address for reaching a specific device, and they change on OS updates, reinstalls, and periodically, so your server must handle refresh and cleanup on every app launch and on platform error responses.
- Offline notifications are queued by the platform and delivered when the device reconnects, subject to the TTL you set, and expired notifications are silently discarded.
- Collapse IDs prevent notification floods by replacing older queued notifications with newer ones that share the same ID, so the user sees one summary instead of twenty individual updates.
- High-priority notifications wake the phone from power-saving mode and should be reserved for direct user actions like messages and calls, because platforms throttle apps that abuse high priority.
- Silent notifications wake the app for background data sync without showing anything to the user, but both platforms throttle them heavily (iOS: ~2 per hour).
- For end-to-end encrypted apps, the push payload contains a trigger (not the actual content), and the app decrypts locally after waking, adding 200-500ms latency but preserving privacy.
Related Concepts
- WebSocket connection management: The persistent connection pattern used by APNs/FCM is analogous to WebSocket connections in web apps. Understanding connection lifecycle, heartbeats, NAT traversal, and reconnection applies to both systems.
- Message queue fan-out: Topic-based push notification delivery is a fan-out pattern similar to Kafka topic consumers or SNS subscriptions. The platform acts as the message broker, handling one-to-many delivery at scale.
- Token-based authentication: Device tokens in push notifications follow similar lifecycle patterns to OAuth tokens (generation, refresh, revocation, expiry). The management challenges (stale tokens, rotation, multi-device) are analogous.
- Rate limiting and throttling: Platform quota management for push notifications uses the same patterns as API rate limiting: token buckets, sliding windows, and exponential backoff on 429 responses.
- Exactly-once delivery: Push notifications offer at-most-once delivery (the platform makes one attempt per online connection, queues when offline, but does not retry indefinitely). Understanding this helps contrast with messaging systems that offer exactly-once or at-least-once guarantees, and explains why important user-facing events should be confirmed through in-app sync, not just push.