Debugging Deliverability

Expired, invalid, mismatched: three dead push tokens that need three different responses

Most platforms collapse every delivery failure into "failed". Three of the most common failures mean completely different things, and treating them the same is how you either lose real subscribers or keep sending into the void.

· 3 min read · Notibase

A push fails. Your dashboard says failed. What do you do?

There is no answer to that question, and that is the problem. Here are three failures that all look identical in a boolean-shaped delivery log, and the correct response to each is different.

The token is gone

APNs 410 Unregistered, FCM UNREGISTERED, web push 404 or 410 Gone.

The device is not coming back. The app was deleted, the browser subscription expired, the user cleared site data. There is nothing wrong with your credentials and nothing wrong with your payload.

Correct response: delete the token. Not disable, not retry — remove it from the audience. A provider that keeps retrying gone tokens is inflating your "sent" count with sends that cannot arrive, and every one of them is a wasted request against your rate limit.

There is a second-order benefit that people miss: your gone rate is an uninstall signal, and a fairly precise one. Watching it move after a release tells you something no analytics SDK can, because uninstalled apps do not send analytics.

The token is invalid

APNs BadDeviceToken or DeviceTokenNotForTopic, FCM INVALID_ARGUMENT on the token field.

The token exists as a string but is unusable in this context. Almost always one of: a sandbox token sent to the production APNs host (or the reverse), a token from your staging app bundle sent under your production topic, or a token that was truncated somewhere in your pipeline.

Correct response: do not delete it — fix your configuration. This is the one that hurts, because a platform that treats invalid the same as gone will happily delete a large slice of a healthy audience the first time somebody deploys with the wrong environment flag. The tokens were fine. The configuration was not, and now they are gone from your database.

A useful tell: gone failures are scattered across your audience over time. Invalid failures arrive in a block, all at once, correlated with a deploy.

Your credentials are wrong

APNs 403 InvalidProviderToken, FCM SENDER_ID_MISMATCH, OAuth 401, web push 403 VapidPkHashMismatch.

Nothing is wrong with any device. Your .p8 was revoked, your service account was deleted, your VAPID keys were regenerated.

Correct response: page somebody. This is the only one of the three that is an outage. Every send is failing, the fix is a credential upload, and the minutes matter. It should not be aggregated into the same counter as a handful of uninstalls.

Why the distinction is not free

Collapsing these is not laziness, it is a design choice with an argument behind it: a single failed state is simpler, and simple states are easier to build dashboards on. The cost only shows up in the specific moment when something has gone wrong and you need to know what — which is, unfortunately, the entire reason the delivery log exists.

Notibase normalises into eight codes rather than one, and keeps the provider's raw response next to each:

CodeMeaningRetry?
address_goneToken permanently dead — device is auto-expiredNo
address_invalidMalformed, or from a different app or environmentNo
address_mismatchSubscriber registered under different VAPID keysNo
payload_invalidContent the provider rejects, usually sizeNo
credentials_invalidYour credentials are wrong or revokedNo
provider_throttledApple or Google asked us to slow downYes, with backoff
provider_downPush service outage or network failureYes
unknownUnmapped — the raw response tells the storyNo

Eight is not a magic number. It is the smallest set where every code implies a different action, which is the only property that makes a taxonomy worth having. unknown exists on purpose: a category that lies is worse than one that admits it does not know, and the raw body is stored either way.

The full reference, including which provider responses map to which code, is in Delivery errors.

What to instrument

If you take one thing from this, make it three separate alerts:

  1. Gone rate, as a slow-moving ratio. Alert on the trend, not the number.
  2. Invalid rate, with a tight threshold and a short window. A spike is a deploy that shipped the wrong environment, and you want to catch it before your cleanup job removes the tokens.
  3. Credentials failures, at any volume above zero. One is enough. It is never a device problem and it is never transient.