Push Unsubscribe Rate Benchmarks and Churn
“Our unsubscribe rate is 0.4%” is a meaningless sentence until you know which of three unrelated events it counts and what sat in the denominator. This reference defines each one, shows how to compute them separately, and explains why a cohort retention curve is a better health signal than any single monthly number.
Quick Answer
Web push loses subscribers through three distinct channels, and each needs its own numerator and its own denominator. Adding them together and dividing by one number produces a figure that cannot be compared to anything, including your own previous month.
| Channel | Numerator | Denominator | You learn about it |
|---|---|---|---|
| Explicit unsubscribe | opt-outs recorded by your app | subscribers notified in the period | Immediately |
| Permission revocation | Notification.permission flipped away from granted |
active subscribers seen in the period | Only on their next visit |
| Silent attrition | endpoints returning 410 Gone or 404 |
endpoints attempted in the period | On the next send attempt |
Report all three, plus a cohort retention curve. A single blended “churn rate” hides which lever to pull.
Three Kinds of Attrition
Explicit unsubscribe is a deliberate act inside your product: a toggle in a preference centre, an “unsubscribe” action button on the notification, or an account-deletion flow. It is the only channel where the user told you why they left, if you asked. It is also the only one you must log for compliance reasons — see GDPR-compliant push unsubscribe logging — and the one you can make friction-free with a one-click unsubscribe action on the notification itself.
Permission revocation happens in browser UI you do not control. The user clicks the padlock and blocks notifications, or resets site permissions wholesale. Your server learns nothing: the subscription may keep returning 201 for a while even though nothing will ever be displayed. The only detection is client-side — read Notification.permission on page load and report a change.
// on every page load for a known subscriber
async function reconcilePermission(subscriberId) {
const state = Notification.permission; // 'granted' | 'denied' | 'default'
const reg = await navigator.serviceWorker.ready;
const sub = await reg.pushManager.getSubscription();
if (state === 'granted' && sub) return;
await fetch('/api/subscriptions/state', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
subscriberId,
permission: state,
hasSubscription: Boolean(sub),
reason: state === 'granted' ? 'subscription-missing' : 'permission-revoked'
})
});
}
Silent attrition is everything else: the browser profile was deleted, the app data was cleared, the device was wiped, the push service garbage-collected an endpoint nobody had used in months. You discover it when the push service answers 410 Gone (or 404 Not Found), and only then. This is usually the largest of the three channels, and the one teams forget to count because it never produces a user-facing event. Retiring those endpoints correctly, and pruning the rows afterwards, is covered in handling 410 Gone responses at scale.
Getting the Denominators Right
Each rate answers a different question, so each divides by a different population.
- Explicit unsubscribe rate answers “of the people we messaged, how many told us to stop?” Divide by subscribers notified at least once in the period, not by total list size. A dormant subscriber who received nothing cannot have opted out of it, and including them makes any send look safer than it was.
- Revocation rate answers “of the people who came back to the site, how many had turned us off?” Divide by subscribers who loaded a page in the period, because that is the only population where you could have observed the state at all. Dividing by the full list produces a number that falls whenever traffic falls, which is exactly backwards.
- Silent attrition rate answers “of the endpoints we tried, how many were dead?” Divide by endpoints attempted. This one is naturally per-send and is the right denominator for spotting a sudden purge by a push service.
-- three rates for one calendar month, each with its own denominator
WITH notified AS (
SELECT DISTINCT subscriber_id FROM push_sends
WHERE sent_at >= date_trunc('month', now()) AND state = 'accepted'
), seen AS (
SELECT DISTINCT subscriber_id FROM page_sessions
WHERE started_at >= date_trunc('month', now())
), attempted AS (
SELECT count(*) AS n FROM push_sends WHERE sent_at >= date_trunc('month', now())
)
SELECT
(SELECT count(*) FROM optouts o
WHERE o.created_at >= date_trunc('month', now())
AND o.subscriber_id IN (SELECT subscriber_id FROM notified))::numeric
/ NULLIF((SELECT count(*) FROM notified), 0) AS explicit_rate,
(SELECT count(*) FROM permission_events p
WHERE p.created_at >= date_trunc('month', now())
AND p.permission <> 'granted'
AND p.subscriber_id IN (SELECT subscriber_id FROM seen))::numeric
/ NULLIF((SELECT count(*) FROM seen), 0) AS revocation_rate,
(SELECT count(*) FROM push_sends s
WHERE s.sent_at >= date_trunc('month', now())
AND s.response_status IN (404, 410))::numeric
/ NULLIF((SELECT n FROM attempted), 0) AS silent_rate;
Cohort Retention and Subscriber Half-Life
A monthly rate is a single scalar describing a population that is constantly being refilled by new subscribers. If acquisition doubles, the blended rate falls even though nothing about retention improved. Cohort curves remove that confound: fix a group by the week they subscribed, and track what share of them are still reachable at each subsequent week.
“Still reachable” means all three conditions hold — no explicit opt-out, permission still granted as far as you know, and the endpoint has not returned 410.
Subscriber half-life — the number of weeks until a cohort is 50% reachable — is the single most useful summary of that curve. It is easy to explain, robust to acquisition volume changes, and directly comparable between acquisition sources. A cohort acquired behind a soft ask that explains what will be sent typically has a much longer half-life than one acquired by prompting on page load, even if both produced the same number of subscriptions on day one.
-- weekly retention for each acquisition cohort
SELECT
date_trunc('week', s.created_at)::date AS cohort_week,
floor(extract(epoch FROM (d.day - s.created_at)) / 604800) AS week_index,
count(*) FILTER (WHERE s.churned_at IS NULL OR s.churned_at > d.day)::numeric
/ count(*) AS reachable_share
FROM push_subscribers s
CROSS JOIN LATERAL generate_series(
date_trunc('week', s.created_at), now(), interval '1 week'
) AS d(day)
GROUP BY 1, 2
ORDER BY 1, 2;
Two habits make these curves trustworthy. Set churned_at from whichever of the three channels fired first, and store the channel alongside it so you can decompose the curve later. And never backfill churned_at with the date you discovered a dead endpoint — a 410 found in March on a subscriber last messaged in November tells you the loss happened somewhere in between, so attribute it to the last successful send, and treat the interval as measurement uncertainty rather than pretending to precision.
What Drives Unsubscribe Spikes
Steady-state attrition is mostly silent and mostly out of your hands. Spikes are almost always yours.
The recurring causes, roughly in order of how often they turn up in incident reviews:
- Cadence breaks. Two or three sends in one day is the fastest way to lose a list. Enforce a hard limit with push notification frequency caps rather than relying on a shared calendar.
- Relevance breaks. A broadcast to the whole list about something only one segment cares about. The unsubscribes concentrate in the segments the message did not apply to, which the daily total hides — always decompose a spike by segment.
- Local-hour mistakes. A send that lands at 02:00 for a subset produces a distinctive spike the following morning. Correct scheduling is covered in timezone-aware push notification scheduling.
- A new prompt going live. A more aggressive permission prompt raises acquisition and lowers cohort quality at the same time, so the blended rate worsens weeks later. This is another reason to read cohorts rather than blends.
- Push service purges. A step change in
410volume across one provider on one day is not your users leaving; it is a provider expiring endpoints. Segment your silent-attrition rate by endpoint host so this is visible. - A re-engagement campaign that overreaches. Win-back sequences to dormant subscribers legitimately produce higher opt-out rates than regular sends; budget for that when planning win-back campaigns for dormant users.
What Counts as Healthy
Be sceptical of any source quoting a precise industry unsubscribe rate. Published figures blend the three channels above, use undisclosed denominators, and come from vendors whose customer mix is nothing like yours. The honest way to state a target is as a range, with the definition attached:
- Explicit unsubscribe per send, over subscribers notified, is typically a fraction of a percent for a relevant, well-paced programme. Anything approaching a full percentage point on a routine send is a cadence or relevance problem worth investigating that week.
- Monthly silent attrition, over endpoints attempted, is usually the largest channel and can comfortably run several percent. It is driven by device and browser lifecycle far more than by your content.
- Subscriber half-life for a healthy consumer list is normally measured in months, not weeks. A half-life under a month means the acquisition flow is producing subscribers who never wanted the notifications.
Set your own thresholds from your own first three months of data, then alert on deviation from your baseline rather than on an absolute number. When you compare periods, use the guardrail discipline described in statistical significance for push notification tests — day-to-day movement in a small numerator is mostly noise.
Gotchas & Edge Cases
- One person, several endpoints. A subscriber with a phone and two laptops holds three subscriptions. Endpoint-level attrition overstates people-level churn; report both and label them clearly.
410on a stale endpoint is old news. The loss happened whenever the browser discarded the subscription, not on the day you tried to send. Attribute it to the last successful send.- Re-subscribing looks like churn plus acquisition. A user who clears site data and re-grants permission produces a new endpoint. Deduplicate on the user id before computing cohort curves, or your churn and acquisition will both be inflated.
- A denied prompt is not churn. Someone who never granted permission was never a subscriber. Keep prompt-conversion metrics entirely separate from attrition metrics.
- Do not delete the row on unsubscribe. Keep a tombstone with the timestamp and channel; you need it for the cohort curve, for suppression, and for the compliance record.
- Seasonality is real. New-device periods after major holidays produce a visible bump in
410volume as old profiles are abandoned. Compare like-for-like periods before declaring a regression.
Related
- Back to Re-Engagement Push Campaign Strategies — the lifecycle programme these retention numbers are meant to steer.
- Win-back push campaigns for dormant users — the intervention to run before a cohort’s curve flattens out.
- Handling 410 Gone responses at scale — the source of the silent-attrition numerator.
- GDPR-compliant push unsubscribe logging — what the explicit opt-out record has to contain.
FAQ
What is a good unsubscribe rate for web push notifications?
There is no single credible industry figure, because published numbers blend explicit opt-outs, permission revocations and dead endpoints over undisclosed denominators. For a relevant, well-paced programme, explicit opt-outs over subscribers notified usually sit in a fraction of a percent per send, while silent attrition over endpoints attempted can run several percent a month. Establish your own baseline in the first three months and alert on deviation from it.
How do I detect when a user revokes notification permission?
Only from the client. Read Notification.permission on page load together with pushManager.getSubscription, and report any state that is not granted-with-a-subscription back to your server. There is no server-side signal for a revoked permission, and the push service may keep accepting requests for an endpoint whose notifications will never be displayed.
What is subscriber half-life and why use it?
It is the number of weeks until an acquisition cohort is 50 percent reachable, counting explicit opt-outs, revocations and dead endpoints together. Unlike a blended monthly rate it does not move when acquisition volume changes, so it compares cleanly between acquisition sources and across time.