Push notifications never arrive for some users
“The send API returns success for everyone, but a slice of users never sees the notification: a few platforms, a few builds, or people who reinstalled.”
ERROR CODES YOU MAY SEE
A code is a clue. Use its meaning and surrounding evidence to narrow the cause.
UNREGISTEREDFirebase Cloud Messaging HTTP v1 · platformHTTP 404: the app instance was unregistered from FCM, so the registration token is no longer valid.
Delete the token on this response. A rising rate after a release suggests the app stopped uploading refreshed tokens; one user with many dead tokens suggests reinstalls.
Official reference for UNREGISTERED (opens in new tab)Code definition reviewedSENDER_ID_MISMATCHFirebase Cloud Messaging HTTP v1 · platformHTTP 403: the authenticated sender ID differs from the sender ID the registration token was issued for.
The token came from a different Firebase project, typically a staging build's google-services.json or GoogleService-Info.plist talking to the production backend.
Official reference for SENDER_ID_MISMATCH (opens in new tab)Code definition reviewed410APNs HTTP/2 responses · platformThe device token is no longer active for the topic (reason Unregistered or ExpiredToken); the response carries a timestamp of when APNs confirmed it.
Stop sending to the token unless the device registers it again later than that timestamp. A 400 BadDeviceToken instead usually means a sandbox token sent to production or the reverse.
Official reference for 410 (opens in new tab)Code definition reviewed
THE PRINCIPLE
A successful send means the provider accepted the message, not that a person saw it. Treat tokens as expiring, per-environment credentials and treat silent pushes as best effort.
FIRST MOVES
- Log provider responses per token. On FCM UNREGISTERED (404) or APNs 410, delete the token; on APNs 400 BadDeviceToken, check you are not sending a sandbox (Xcode) token to api.push.apple.com or a TestFlight/App Store token to the sandbox host.
- Upload the token on every app start and on the refresh callback, keyed by install, with a last-seen timestamp. FCM treats registrations idle for a month as stale and expires Android ones after 270 days; prune your own stale rows.
- Check permission before blaming delivery: since Android 13 POST_NOTIFICATIONS is off by default for new installs, iOS needs requestAuthorization, and on Android a channel the user turned off, or a channel ID that was never created, shows nothing. A channel created with low importance stays low: the app can't raise it later, so ship a new channel ID instead.
- Do not rely on silent pushes for anything that must happen. iOS treats content-available pushes as low priority, throttles them (Apple says no more than two or three per hour), and drops them for apps the user force-quit.
- On Android, high-priority FCM messages that never produce a visible notification get deprioritized; send user-facing alerts as notification messages and keep data-only messages for sync (the AndroidConfigV2 preview makes this explicit with RemoteNotification and BackgroundSync types).
TOOLS: THEN → NOW
PATTERN SNAPSHOT
// Server: prune tokens from provider feedback, never on a guess.
async function handleSendResult(token: DeviceToken, res: ProviderResult) {
if (res.provider === "fcm" && res.errorCode === "UNREGISTERED") return tokens.delete(token.id);
if (res.provider === "apns" && res.status === 410) {
// Only delete if the device has not re-registered since APNs invalidated it.
if (token.lastSeenAt.getTime() < res.timestamp) return tokens.delete(token.id);
}
if (res.provider === "apns" && res.reason === "BadDeviceToken") {
metrics.increment("apns.env_mismatch", { build: token.buildChannel });
}
}CLOSE THE AI. EXPLAIN THIS.
Why can a push sent to a TestFlight user fail with BadDeviceToken while the same code works from Xcode?HOW IT WORKS UNDERNEATH
RELATED SYMPTOMS
SOURCES
- Firebase: Best practices for FCM registration token management (opens in new tab)Checked
- Firebase: Setting the priority of a message (Android) (opens in new tab)Checked
- Apple: Handling notification responses from APNs (opens in new tab)Checked
- Apple: Pushing background updates to your App (opens in new tab)Checked
- Android Developers: Notification runtime permission (opens in new tab)Checked
Guide reviewed