Recovering Users Who Blocked Push Permission

This guide shows the concrete steps to bring back users whose browser reports Notification.permission === 'denied' — detect the block, guide a site-settings reset, nudge them with the right UI, and measure how many actually return.

Quick answer

You cannot re-open the native prompt once a user has blocked notifications. The state is terminal for requestPermission(), which resolves instantly with denied and shows nothing. Recovery means three things: detect denied silently, render an in-page nudge that links to browser site settings with the correct per-browser path, then re-read permission on focus and re-subscribe the instant it returns to granted. Treat anything else — iframes, subdomains, repeated calls — as a dead end.

What you want What actually works
Re-show the OS prompt Not possible from script — guide to site settings
Detect the block Read Notification.permission (synchronous, no UI)
Reverse the block User toggles the per-site setting in their browser
Know it worked Re-check permission on focus / visibilitychange, then re-subscribe

Drawn out, the asymmetry is stark. One path terminates at a wall your code cannot see past; the other runs through UI you do not own but can absolutely explain.

The script path versus the browser-chrome path out of denied From a cached denied state, calling requestPermission again hits a hard wall and resolves instantly with denied while rendering nothing. The only working path runs from an in-page nudge to the browser site-settings panel, where the user switches notifications to Allow, after which the page can re-subscribe. Two paths out of denied — only one of them exists The cached value is per-origin, so reloads, new tabs and fresh service workers all inherit it. denied cached for the origin Script path — a dead end requestPermission() called again Resolves instantly with denied no dialog is ever rendered Iframes, sibling subdomains and repeat calls all end at the same wall. Chrome path — the only reversal In-page nudge explains the reset Browser site settings user flips it to Allow granted re-subscribe Everything you build for a blocked user lives on the lower lane: detect, explain, watch, re-subscribe. The wall is anti-nag behaviour shared by Chromium, Gecko and WebKit — treat it as a fixed constraint.
The upper lane is closed by design; the lower lane is entirely informational, which is why recovery is a copy and timing problem rather than an API problem.

Why the block is sticky

When a user clicks Block, or when Chrome’s repeat-dismissal heuristic auto-denies, the browser caches denied for your origin and suppresses the native dialog indefinitely. This is anti-nag behaviour shared by Chromium, Gecko, and WebKit, and there is no permitted way around it. The deny-detection mechanics — including telling an explicit block apart from an auto-block — are covered in detecting denied push permission without prompting. The full state machine and the architectural framing live in the parent re-permission and recovery flows guide.

Because the reversal happens in browser chrome you do not control, your entire recovery surface is informational: detect, explain, watch, re-subscribe.

It is worth being precise about what “blocked” actually covers, because the population is more mixed than it looks. Some of these users clicked Block deliberately and mean it. Others dismissed the dialog twice and were auto-blocked by Chrome’s heuristics without ever consciously rejecting you. A third group accepted notifications on a different device and never on this one. And a fourth — easy to forget — never actually blocked anything; their browser simply does not support push, or an OS-level toggle is suppressing delivery. Lumping all of these into a single “denied” bucket and showing the same apologetic banner to everyone is how recovery flows annoy people. The detection step below separates the genuinely-blocked from the never-supported, and your copy should stay neutral enough to fit both the deliberate blocker and the accidental one.

Detection snippet

Read the permission state directly. It never triggers UI, so run it on every page load and branch on the result.

function pushBlockStatus() {
  if (!('Notification' in window) || !('PushManager' in window)) {
    return 'unsupported';
  }
  return Notification.permission; // 'default' | 'granted' | 'denied'
}

if (pushBlockStatus() === 'denied') {
  showRecoveryNudge();   // in-page UI only — never call requestPermission() here
}

The contextual nudge UI

The nudge is a small, dismissible in-page element that appears only for blocked users and only at a relevant moment — for example after the user clicks an action that would have benefited from a notification. It explains the situation in plain language and reveals the per-browser reset steps on demand.

function showRecoveryNudge() {
  if (sessionStorage.getItem('push_nudge_seen') === '1') return;
  sessionStorage.setItem('push_nudge_seen', '1');

  const steps = resetStepsForBrowser();
  const el = document.createElement('div');
  el.setAttribute('role', 'dialog');
  el.setAttribute('aria-label', 'Re-enable notifications');
  el.className = 'push-recovery-nudge';
  el.innerHTML = `
    <p>Notifications are <strong>blocked</strong> for this site. To turn them back on:</p>
    <p class="steps">${steps}</p>
    <button type="button" data-close>Got it</button>
  `;
  el.querySelector('[data-close]').addEventListener('click', () => el.remove());
  document.body.appendChild(el);

  watchForReEnable();
}

function resetStepsForBrowser() {
  const ua = navigator.userAgent;
  if (/Firefox\//.test(ua)) {
    return 'Click the padlock in the address bar, clear the "Send Notifications" Blocked entry, then reload.';
  }
  if (/Edg\//.test(ua)) {
    return 'Click the padlock → Permissions for this site → Notifications → Allow, then reload.';
  }
  if (/Chrome\//.test(ua)) {
    return 'Click the tune/padlock icon at the left of the address bar → Site settings → Notifications → Allow, then reload.';
  }
  if (/Safari\//.test(ua)) {
    return 'Open Safari → Settings → Websites → Notifications, set this site to Allow. On iOS also check Settings → Notifications.';
  }
  return 'Open your browser\'s site settings for this page and change Notifications from Block to Allow, then reload.';
}

Three details in that markup carry most of the conversion: where the card points, how the headline is framed, and what the button honestly promises. The wording of the instructions themselves is a deeper subject, covered in guiding users to reset a blocked notification permission.

Anatomy of a recovery nudge A browser window mock with the address-bar padlock marked as annotation one, the nudge card headline and body marked as annotation two, and the call-to-action button marked as annotation three. A legend beneath explains that the padlock is the only control that can unblock the origin, that value-led copy outperforms an error message, and that the button only reveals instructions. Anatomy of a recovery nudge example.com/watchlist 1 Notifications are blocked Turn them back on to hear the moment your price alert triggers. × Show me how 2 3 1 — The padlock or tune icon is the only control on screen that can unblock the origin. 2 — Lead with the value, not the fault. "Notifications are blocked" alone reads as an error and earns a shrug. 3 — The button reveals per-browser steps. It cannot change the permission, so never label it "Allow".
The nudge card annotated: the padlock it points at, the value-led headline, and a button that promises instructions rather than a permission change.

Numbered recovery steps

  1. Detect silently. On load, read Notification.permission. Continue only if it is denied. Never call requestPermission() for a blocked origin — it does nothing but burn a no-op.
  2. Pick the moment. Show the nudge at a contextually relevant point (a save, a follow, a price-watch action), not on first paint. Relevance is what motivates someone to dig into browser settings.
  3. Show the exact path. Sniff the user agent and render the correct per-browser instructions. Vague “check your settings” copy fails; the precise path converts.
  4. Watch for the flip. Attach visibilitychange and focus listeners. Users edit the setting in another surface and tab back; that is your signal to re-read permission.
  5. Re-subscribe immediately. When permission reads granted, call pushManager.subscribe() with your VAPID public key and POST the new endpoint to the server. The old subscription was discarded when permission was lost, so a fresh subscribe is mandatory.
function watchForReEnable() {
  const onReturn = async () => {
    if (Notification.permission !== 'granted') return;
    document.removeEventListener('visibilitychange', onReturn);
    window.removeEventListener('focus', onReturn);

    const reg = await navigator.serviceWorker.ready;
    const sub = await reg.pushManager.getSubscription()
      || await reg.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array(window.__VAPID_PUBLIC_KEY__)
      });

    await fetch('/api/push/subscribe', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ subscription: sub, recovered: true })
    });
    track('push_recovered');
  };
  document.addEventListener('visibilitychange', onReturn);
  window.addEventListener('focus', onReturn);
}

Measuring recovery

Instrument three events to compute a recovery funnel: push_blocked_detected (a denied user saw the nudge), push_reset_steps_viewed (they expanded the instructions), and push_recovered (permission returned to granted and re-subscribe succeeded). Recovery rate is push_recovered / push_blocked_detected. Realistic rates are low — single-digit percentages — so judge the nudge by lift over zero, not by absolute numbers. Send the recovered: true flag with the new subscription so backend analytics can separate fresh opt-ins from recoveries.

Recovery funnel across the three instrumentation events All blocked users who saw the nudge form the baseline. Roughly a fifth expand the reset instructions, and a small single-digit share complete the reset and re-subscribe. Each drop-off points at a different fix: framing and timing for the first gap, wrong instructions or an operating-system block for the second. Recovery funnel — where blocked users fall out Illustrative shape from a well-instrumented consumer flow; your absolute numbers will differ. push_blocked_detected 100 % of blocked users who were nudged push_reset_steps_viewed 22 % a large gap here means the copy or placement is not earning the click push_recovered 6 % a large gap here means wrong instructions, or an OS block they cannot see 0 25 % 50 % 75 % 100 % Recovery rate = push_recovered / push_blocked_detected. Single digits are normal. Segment by browser and platform — desktop Chrome and iOS Safari behave nothing alike.
The two drop-offs in the recovery funnel have different causes, so measure them separately rather than tracking a single recovery rate.

The funnel between those three events tells you where to invest. A large gap between push_blocked_detected and push_reset_steps_viewed means your banner copy or placement is not motivating people to even look at the instructions — work on framing and timing. A large gap between push_reset_steps_viewed and push_recovered means people are reading the steps but not completing them, which usually points at instructions that are wrong for their browser or an OS-level block they cannot see. Segment the funnel by browser and platform; recovery behaviour on desktop Chrome looks nothing like recovery on iOS Safari, and a blended number hides both stories. Finally, watch the time-to-recovery distribution: most genuine recoveries happen within the same session as the nudge, so a steady trickle of “recovered days later” often indicates the browser restored a subscription on its own rather than your flow doing the work.

Be honest with yourself about ceilings. Even a well-built recovery flow recaptures only a slice of blocked users, because a denial is a real signal of disinterest for many of them. The point is not to chase a high percentage; it is to recover the subset who genuinely changed their mind, cheaply and without nagging the rest. Pair the push recovery number with how many blocked users accepted an alternative channel instead — that combined figure is the true measure of how much of the “lost” audience you brought back.

Gotchas & edge cases

  • OS-level blocks masquerade as success. A user can set the site to Allow yet still see nothing because the OS suppresses notifications (Windows Focus Assist, macOS Do Not Disturb, iOS per-app toggle). Add a line reminding users to check OS notification settings.
  • Never sniff your way into the wrong instructions. Embedded webviews and brand forks defeat user-agent detection. Always keep a generic fallback and consider linking to the browser’s own help page.
  • iOS needs the Home Screen install. Safari on iOS only delivers web push to sites added to the Home Screen as a web app. A blocked iOS user may actually be an uninstalled one; detect display-mode: standalone before promising push.
  • Re-subscribe can collide with a rotated VAPID key. If your keys changed between the original subscribe and the recovery, the new subscribe fails or produces an endpoint the server rejects. Coordinate per rotating VAPID keys without losing subscribers.
  • Do not nag. Cap the nudge to once per session (or once per week via localStorage). Users who decline twice should be routed to your opt-out and preference centers and left alone.

FAQ

How many blocked users can a good recovery flow realistically bring back?

Single-digit percentages of the users who see the nudge. A denial is a genuine signal of disinterest for most of that population, and the reversal requires opening a settings panel they have probably never opened before. Judge the flow by lift over zero and by the combined figure — push recoveries plus blocked users who accepted an alternative channel — rather than by the raw recovery rate.

Can I detect that a user opened the browser's site-settings panel?

No. The panel is browser chrome and is completely opaque to the page — no event, no focus change you can attribute to it, no Permissions API notification. The closest proxy is a visibilitychange or focus event followed by a permission re-read that now returns granted. Instrument the click on your own “how to re-enable” control as the intent signal instead.

Should the recovery nudge appear on every page or only on high-intent actions?

Only on high-intent actions. A blocked user has already declined once, so a site-wide banner reads as nagging and costs more trust than it recovers. Attach the nudge to the moment a notification would obviously have helped — a price watch, a follow, a saved search — and cap it at once per session, with a longer localStorage cooldown for users who dismiss it twice.