Walk through push notification delivery end to end — FCM/APNs, the OS, and your app
Your server never talks to the device — it talks to Google or Apple's relay, which holds a socket your app doesn't even need to be running for.
LEARN BY DOINGPut your intuition to work3 steps
Walk through push notification delivery end to end — FCM/APNs, the OS, and your app
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.#The one fact that explains everything else
Your backend never opens a connection to a phone. It can't — devices move between networks, sleep their radios, and sit behind NAT with no stable address to dial. So both Apple and Google solved it the same way: the device's operating system keeps one long-lived, persistent connection open to a relay service (APNs for Apple, FCM's connection server for Android), and your server's only job is to hand a message to that relay and trust it to deliver. This is why push works even when your app process doesn't exist — the socket belongs to the OS, not to you.
#Getting a token before you can send anything
Before your server can target a specific device, the device has to register. On iOS, the app calls registerForRemoteNotifications(), which asks the OS to establish identity with APNs, and the OS calls back with a device token. On Android, the FCM SDK does the equivalent work under the hood and hands your app an FCM registration token. Either way, the flow is the same: app asks OS for a token, OS talks to the push service to mint one, app receives it, and app sends it up to your backend where it gets tied to a user id. That token is not permanent — it rotates on reinstall, on restoring from backup to a new device, and periodically for other reasons — so your backend has to treat "send failed, token invalid" as a normal, expected response and prune it rather than retry forever.
#What your server actually sends
A push payload is small on purpose — it's a wake-up signal, not a data channel. FCM's HTTP v1 API and APNs' HTTP/2 API both accept a payload split conceptually into a notification part (title, body — the OS can render this itself without waking your code) and a data part (arbitrary key-value pairs your app interprets). You also set priority (normal vs. high/immediate — normal can be deferred by Doze, high asks for prompt delivery), a time-to-live so a stale message expires instead of arriving hours late, and on iOS a payload flag (content-available: 1) for a "silent" push meant to wake the app briefly for background work rather than show anything to the user.
#The three states that change everything
The same message is handled completely differently depending on what your app is doing when it arrives, and this is the part interviewers actually want to hear you reason through out loud.
When the app is foreground, the OS hands the payload straight to your code — on Android that's FirebaseMessagingService.onMessageReceived, on iOS it's the UNUserNotificationCenterDelegate's willPresent callback — and it's entirely up to your app to decide whether to show an in-app banner, update a badge, or do nothing at all, because the system won't put a notification in the tray for something the user is already looking at. When the app is backgrounded but alive, the OS renders the tray notification itself from the "notification" part of the payload without touching your code at all; if the payload is a silent/data push, the OS may grant a short wake window to let your app do useful background work before the user even taps anything. When the app is killed, the OS still owns the persistent connection and still renders the tray notification — your code runs zero instructions until the user taps it, which cold-starts the app with the notification's payload available to read on launch.
#The failure mode everyone eventually hits
Silent, data-only pushes used to sync content are the thing that quietly stops working in production and confuses every engineer who didn't build the push path themselves. iOS enforces both a rate budget on silent pushes and, more importantly, suspends background delivery to an app the user has force-quit by swiping it away — from the OS's perspective, that gesture is the user saying "stop running this," and it takes that literally until the user opens the app again. Android's Doze mode similarly defers normal-priority FCM messages until the device's next maintenance window, though high-priority messages are allowed a brief exemption. None of this is a bug in your integration — it's both platforms refusing to let push become a backdoor for apps to run indefinitely in the background, and it means push is fundamentally a best-effort, unordered, at-least-once wake-up signal, never a guaranteed delivery channel for data your app can't re-fetch some other way.