Every push notification system eventually develops a confidence problem. Not a technical one — a measurement one. The pipeline reports high send rates, the dashboards look healthy, and somewhere downstream a user is wondering why they never hear from your app anymore.
The lie is structural. It starts with treating a successful HTTP 200 from APNs or FCM as the finish line.
"Sent" Is a Local Variable
When your service hands a notification to a provider, you've completed exactly one hop in a multi-hop delivery chain. The provider accepted your payload. That's it. What happens between that acceptance and the moment a user taps the notification is a sequence of steps your code has no direct visibility into.
APNs, FCM, and SMS gateways don't share a common delivery contract. Each one has its own error taxonomy, its own retry semantics, and its own definition of what "delivered" means. FCM's UNREGISTERED error is not the same class of problem as APNs' 410 status. An SMS provider's DELIVRD receipt is not equivalent to a mobile OS actually surfacing the alert. Treating these as interchangeable signals is the first place accuracy erodes.
The practical consequence: your "sent" counter is a count of provider acceptances, not a count of notifications that reached users. Those two numbers can diverge dramatically, and they will diverge silently unless you build for it.
Exactly-Once Is a Fantasy, So Stop Designing for It
There's a common impulse to want exactly-once delivery guarantees across the full chain. It's the right instinct and the wrong target. Across external provider boundaries, exactly-once is architecturally unavailable to you. You do not control the network between your service and APNs. You do not control whether a provider acknowledgment makes it back before a timeout. You do not control whether a retry triggers a duplicate processing event downstream.
What you can control is your own idempotency.
At-least-once delivery with deduplication on both sides is the only guarantee worth designing around. In practice, this means maintaining a delivery ledger — a durable record of dispatch attempts keyed to a stable notification ID. Before dispatching, write an attempt. After receiving a provider response, update the record. On retry, check the ledger first.
A minimal version of this looks something like:
def dispatch_notification(notification_id, payload, recipient_token):
if ledger.already_dispatched(notification_id):
return ledger.get_result(notification_id)
ledger.record_attempt(notification_id, status="pending")
try:
response = provider_client.send(recipient_token, payload)
ledger.update(notification_id, status="accepted", provider_response=response)
return response
except ProviderError as e:
ledger.update(notification_id, status="failed", error=str(e))
raise
This is not sophisticated infrastructure. It's a discipline. The ledger doesn't need to be exotic — a database table with a unique constraint on notification_id does the job. What it gives you is a system that knows what it actually did, not just what it intended to do.
Token Rot Is Where Silent Failures Accumulate
The most insidious failure mode in push infrastructure isn't a crash or a network error. It's successful sends to dead tokens.
Device tokens expire. Users uninstall apps. OS upgrades invalidate registrations. APNs will return a 410 status with a timestamp indicating when a token became invalid. FCM will return UNREGISTERED. These are not transient errors — they are permanent feedback that a specific token should be retired.
Ignoring this feedback is how a notification system builds up a population of ghost recipients. Your send volume stays constant. Your delivery rate falls. Your system reports nothing unusual because you never asked it to track the gap.
The fix is a feedback loop, not a one-time cleanup. When a provider returns an invalidation signal, your system needs to act on it immediately: mark the token as inactive, stop dispatching to it, and if your architecture supports it, trigger a re-registration flow the next time that user opens the app.
Token lifecycle management is not a housekeeping task. It's a first-class part of your delivery model. A notification pipeline that doesn't consume provider feedback is measuring its own performance with a broken instrument.
What Reliable Actually Means
A reliable push notification architecture is not one that sends a lot of notifications. It's one that knows the difference between sending and delivering, accounts for the gaps between providers, and updates its own state based on what providers tell it.
That means three concrete commitments: model delivery as a multi-step process with observable state at each boundary, use a delivery ledger and stable notification IDs to make retries safe, and treat every provider feedback signal as data you are required to act on.
The dashboard that shows you a high send rate while your actual delivery rate quietly decays is not a success metric. It's a measurement system that hasn't been honest with itself yet.
Top comments (0)