Web push received but not shown
Why a web push arrives but no notification shows, or Chrome shows its generic notice: waitUntil, userVisibleOnly, tag and renotify, and OS settings.
Your server got 201 Created, the push reached the browser, and still nothing useful appeared. Either nothing at all, or Chrome’s generic “This site has been updated in the background”. From here on the problem is in the service worker or the device, not the sender. This page goes through each cause in the order to check them.
Symptom to cause
| What you see | Likely cause | Fix |
|---|---|---|
| “This site has been updated in the background” | No showNotification() before event.waitUntil() settled |
Show a notification on every push, inside waitUntil |
| Nothing, no error | showNotification() called but its promise not passed to waitUntil |
Pass the promise; the worker can stop before it runs |
Nothing, TypeError in the worker console |
Permission denied, or the worker is not active yet | Check Notification.permission; wait for the worker to activate |
subscribe() fails in Chrome or Edge |
userVisibleOnly missing or false |
Subscribe with userVisibleOnly: true |
| A new push replaces the old one without a sound | Same tag, renotify not set |
Add renotify: true, or use a different tag |
| Works on desktop, not on iPhone | Site not added to the Home Screen | See web push on iOS |
| Arrives late or never | Browser closed or computer asleep past the TTL |
A longer TTL, or Urgency: high for messages people wait for |
“This site has been updated in the background”
Chrome requires a visible notification for every push. If your push handler does not show one before the promise you passed to event.waitUntil() settles, Chrome shows its own notice with that text. The fix is always the same shape:
self.addEventListener("push", (event) => {
const n = event.data ? event.data.json() : {};
event.waitUntil(
self.registration.showNotification(n.title || "New message", { body: n.body || "", data: { url: n.url } })
);
});
Three ways code that looks right still triggers it:
- A branch that skips the notification. For example
if (document is focused) return;logic, or “only show it if the data is new”. Every branch must end inshowNotification(). fetchfirst, notification after, without one chain. If you fetch content and then show it, return the whole chain towaitUntil, and show a fallback notification incatchso a failed fetch still shows something.- An exception in the handler. A bad
JSON.parseon a text payload throws before anything is shown. Wrap it.
self.addEventListener("push", (event) => {
event.waitUntil((async () => {
try {
const { id } = event.data.json();
const msg = await fetch(`/api/messages/${id}`).then((r) => r.json());
await self.registration.showNotification(msg.title, { body: msg.body });
} catch {
await self.registration.showNotification("You have a new message");
}
})());
});
Silent pushes: userVisibleOnly
pushManager.subscribe() takes userVisibleOnly, a promise that every push will lead to something the user can see. Chrome and Edge reject the subscription unless it is true, so a data-only web push that updates a cache and shows nothing is not possible there. Firefox lets a site receive a limited number of pushes without a notification; pushes that show one are exempt, and the quota refreshes each time the user visits the site.
const sub = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey });
If you need to sync data in the background, show a notification as part of the same push, or sync the next time the page opens.
The notification is shown but nobody notices
tagreplaces quietly. A notification with the sametagreplaces the older one, and by default the replacement makes no sound. Setrenotify: truewith the tag when the user should hear it again.silent: trueturns off sound and vibration whatever the device settings are.- It closes on its own. Use
requireInteraction: trueif it must stay until the user clicks or dismisses it. - The operating system hides it. macOS and Windows have their own notification switch per browser, and Focus or Do Not Disturb hides banners. If the web push tester cannot show a notification either, the cause is here, not in your code.
Check the service worker itself
- Open DevTools > Application > Service workers. The worker must be activated for your page’s scope, and the Push button there sends it a test push event.
showNotification()throws aTypeErrorwhile the worker is notactivatingoractivated, and when the user has denied permission.- A worker at
/js/sw.jsonly controls/js/. Serve it from the root, or set theService-Worker-Allowedheader. - After editing the worker, the old one keeps running until every tab using it closes. Tick “Update on reload” while testing.
With SignOps
SignOps Push sends the title and body you give it in the push, so your worker always has something to show. Its ready-made worker (two lines in your sw.js, see the web push guide) calls showNotification() inside waitUntil for every push that has a title or body, draws the image and up to two buttons, and opens the link on click. A push you send with neither is left to your own code, which on Chrome must still show something. The @signops/web library subscribes with userVisibleOnly: true. To rule out your server, the web push tester sends a push to this browser with no account, and web push errors covers the status codes before the push reaches the browser.
Sources: web.dev: Push events, MDN: PushManager.subscribe(), MDN: Push API, MDN: showNotification().