skip to content

A service that sends web push notifications sees delivery quietly decline over months, with the push service answering 404 and 410 for many stored endpoints. Why do push subscriptions become invalid, and how should the client and server handle it?

level: seniorimportance: should knowfreq 26%

answer

  1. not a durable user identifier
  2. the endpoint is the primary key
  3. two status codes mean delete
  4. one key rotation kills them all
  5. reconcile on every launch

basics

~20 s

Push subscriptions are perishable: revoked permission, cleared site data, browser reinstalls, a rotated VAPID key or push-service expiry all invalidate them. Servers must delete endpoints that return 404 or 410, and clients should re-read getSubscription() on each launch and re-register it.

solid answer

~40 s

A `PushSubscription` is not a durable user identifier. It dies when the user revokes notification permission, clears site data, uninstalls the app, or when the push service rotates or expires the endpoint; changing your VAPID application server key invalidates every existing subscription at once. The push service signals a dead endpoint with 404 or 410, and the only correct response is to delete that row — retrying wastes quota and can get your sender throttled. On the client, treat the server as needing reconfirmation: on every launch, read `registration.pushManager.getSubscription()` and re-POST it if it differs from what you last sent. Service workers may also receive a `pushsubscriptionchange` event, though support is uneven, so the launch-time reconciliation is the reliable path.

go deeper

for a junior

Understand that a push subscription can stop working without any notice to your server, and that the user can turn notifications off at any time from browser or OS settings.

for a middle

Name the concrete invalidation causes and explain that 404 and 410 from the push endpoint mean delete the record rather than retry it.

for a senior

Show the operational loop: instrument accepted-versus-gone ratios, reconcile getSubscription() at launch, key storage by endpoint, and treat one user as having several perishable channels.

for a principal

Own the design consequence — push is a best-effort channel, so the notification pipeline must degrade to in-app delivery, and VAPID key management becomes long-lived infrastructure whose rotation is a migration, not a config change.

## Why subscriptions rot Teams treat the endpoint like a user id stored once at signup, and then are surprised that a cohort's delivery rate decays. Every one of these breaks a subscription: - **Permission revoked.** The user turns notifications off in browser or OS settings. The subscription is dropped. - **Site data cleared.** Clearing storage for the origin removes the service worker registration and with it the subscription. - **App or browser reinstalled**, or the profile recreated on a new device. Subscriptions are per browser instance, never per person. - **Push service expiry.** The endpoint can simply stop being valid; `subscription.expirationTime` occasionally carries a timestamp for this, though it is usually `null`. - **VAPID key rotation.** The subscription is bound to the `applicationServerKey` supplied when it was created. Rotate your key pair and every existing subscription becomes unusable — a self-inflicted mass invalidation, and a reason to treat that key as long-lived infrastructure. - **Browser housekeeping.** Long-unused sites can lose background privileges, and storage eviction under pressure takes the registration with it. ## Server-side: honour the status codes The Web Push protocol communicates subscription health through the HTTP response from the endpoint: - **404 / 410** — the subscription no longer exists. Delete it. This is the single most important rule, and skipping it means an ever-growing table of dead endpoints, wasted send capacity, and delivery metrics that look like a product problem rather than a hygiene problem. - **429** — you are being rate-limited; back off and honour `Retry-After`. - **413** — the payload is too large; the practical ceiling is around 4KB after encryption. - **400 / 403** — usually a malformed request or a VAPID signature the push service will not accept; a code bug, not a user state. A reasonable sender therefore looks like: ```js const res = await sendWebPush(subscription, payload); if (res.status === 404 || res.status === 410) { await db.pushSubscriptions.delete({ endpoint: subscription.endpoint }); } else if (res.status === 429) { await scheduleRetry(res.headers.get('retry-after')); } ``` Store the endpoint as the unique key. The same user on the same device can produce a new endpoint after a reinstall, and keying on user id alone silently overwrites or duplicates rows. ## Client-side: reconcile, do not assume The browser will not tell your server anything. Make reconciliation part of startup: ```js const registration = await navigator.serviceWorker.ready; const subscription = await registration.pushManager.getSubscription(); if (!subscription) { // permission may still be granted but the subscription is gone; resubscribe } else if (subscription.endpoint !== lastSentEndpoint) { await postSubscriptionToServer(subscription); } ``` Also check `Notification.permission` — a user who set it to `denied` should not be asked again, and should ideally see an in-app explanation of how to re-enable it, since no API can reopen that decision. ## pushsubscriptionchange The spec defines a `pushsubscriptionchange` event fired in the service worker when the browser invalidates or refreshes a subscription: ```js self.addEventListener('pushsubscriptionchange', (event) => { event.waitUntil( self.registration.pushManager .subscribe(event.oldSubscription?.options ?? DEFAULT_OPTIONS) .then((sub) => postSubscriptionToServer(sub)) ); }); ``` It is the mechanically right answer, but browser support and the exact circumstances under which it fires have historically been uneven, and it obviously cannot fire when the user has revoked permission outright. Implement it, and still keep the startup reconciliation as the path you actually depend on. ## Measuring it The symptom in the question — a slow decline — is only visible if you instrument it. Track, per send batch, the ratio of 201-class accepted responses to 404/410 deletions, and the age distribution of stored subscriptions. A healthy population shows steady churn with steady replacement; a population that only ages is one where reconciliation on the client is missing, and it will decay to nothing no matter how good the notification content is. ## The judgment call Because subscriptions are per browser instance and perishable, they are the wrong place to hold anything of record. Store them as a delivery channel attached to a user, expect several per user, expect them to die, and design the notification pipeline so a failed push degrades to an in-app message the user sees on next open rather than a lost event.

  • Why does rotating your VAPID key pair break every existing subscription?
    The subscription is created with a specific `applicationServerKey`, and the push service ties the endpoint to that identity. Requests signed by a different key are rejected. Rotation therefore means all users must resubscribe, so plan key management as long-lived infrastructure and never rotate casually or per environment.
  • A user has `Notification.permission === 'denied'`. What can your code do about it?
    Nothing directly — a denied permission cannot be re-requested by script, and calling `requestPermission()` resolves immediately as denied. The only path is an in-app explanation pointing the user at browser or OS settings, and it is worth surfacing that state so you stop counting the user as reachable.
  • Should you key stored subscriptions by user id or by endpoint?
    By endpoint, with user id as a foreign key. One user routinely has several live subscriptions — different devices and browsers — and reinstalls produce new endpoints for the same device. Keying on user id either overwrites valid channels or creates silent duplicates that both receive the same notification.

saying these in an interview costs you the question

  • Treating the push endpoint as a stable per-user identifier
  • Retrying 410 responses instead of deleting the subscription
  • Assuming permission granted once stays granted forever
  • Rotating VAPID keys per deploy or per environment
  • Relying solely on pushsubscriptionchange for recovery

context