Web Push Notifications: How the Push API Works
The Push API lets a web app send notifications through a service worker, even when the site isn't open in a browser tab. Here's the full flow.
The Push API is the browser API that lets a web app receive messages from a server and show a notification, even when the site isn’t open in a tab or the browser isn’t running in the foreground. It’s the mechanism behind “enable notifications” prompts on news sites, chat apps, and web-based email clients, and it works without any native app install.
Push notifications are often confused with browser notifications in general, but they’re two separate pieces working together: the Notifications API displays a notification on screen, and the Push API delivers a message that can trigger one even when your page isn’t loaded. You can use the Notifications API alone while a tab is open; you need the Push API to reach a user who has closed the tab entirely.
The three pieces
Web push relies on three cooperating parties:
- Your service worker — a script that runs in the background, independent of any open page, registered per the same lifecycle covered in what a service worker is. It’s the piece that receives push events and decides what to show.
- The browser’s push service — an intermediary run by the browser vendor (not by you or the user) that queues and delivers messages to the device. Your server never talks directly to the user’s browser; it talks to this push service, which routes the message.
- Your application server — the backend that decides when to send a notification and pushes the payload to the browser’s push service using a subscription endpoint the browser gave you.
This is a deliberately different model from a raw persistent connection like a WebSocket or Server-Sent Events stream. Push works even when nothing is connected, because delivery is handled by the OS and browser vendor’s infrastructure in the background, similar in spirit to how native mobile push works.
Subscribing a user
Before you can push anything, the user has to opt in and the browser has to hand you a subscription object. The flow looks roughly like this:
const registration = await navigator.serviceWorker.register("/sw.js");
const permission = await Notification.requestPermission();
if (permission !== "granted") return;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: VAPID_PUBLIC_KEY,
});
// Send `subscription` to your backend and store it against the user.
await fetch("/api/push/subscribe", {
method: "POST",
body: JSON.stringify(subscription),
});
userVisibleOnly: true is a browser requirement — it’s a promise that every push you send will result in a visible notification, so sites can’t silently wake up in the background without the user knowing. applicationServerKey is your VAPID (Voluntary Application Server Identification) public key, which identifies your server to the push service and lets it verify that push messages actually came from you.
The returned subscription object contains an endpoint URL (unique to that browser and device) plus encryption keys. Store it against the logged-in user server-side — it’s what you’ll send messages to later.
Sending a push message
On the server, sending a notification means POSTing an encrypted payload to the subscription’s endpoint, signed with your VAPID private key. Most backends use a library rather than implementing the Web Push protocol’s payload encryption by hand, since it involves per-message key negotiation. Conceptually, though, it’s just an authenticated HTTP request to the browser vendor’s push service, which then delivers it to the right device.
Handling the push event
Inside the service worker, a push event fires when a message arrives — even if no tab for your site is open:
self.addEventListener("push", (event) => {
const data = event.data.json();
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: "/icon.png",
}),
);
});
self.addEventListener("notificationclick", (event) => {
event.notification.close();
event.waitUntil(clients.openWindow(data.url));
});
event.waitUntil() tells the browser to keep the service worker alive until the promise resolves — without it, the browser could terminate the worker before showNotification() finishes. The separate notificationclick handler controls what happens when the user actually taps the notification, typically opening or focusing a relevant page.
Push API vs. alternatives
| Push API | WebSocket / SSE | Polling | |
|---|---|---|---|
| Works with tab closed | Yes | No | No |
| Connection required | No — delivered via OS/browser push service | Yes — persistent connection | No, but repeated requests |
| Best for | Re-engagement, alerts, async events | Live in-app updates while open | Simple, infrequent checks |
| Battery/network cost | Low — handled by OS push infrastructure | Moderate — held-open connection | Depends on interval |
They’re not mutually exclusive. A chat app might use a WebSocket for live message delivery while a tab is focused, and fall back to push notifications to alert the user when it isn’t.
Permission UX and pitfalls
Notification permission prompts are one of the most over-triggered browser dialogs on the web, and browsers have responded by making unsolicited prompts easy for users to dismiss and hard to reverse. A few practices that matter in practice:
- Ask in context. Trigger
Notification.requestPermission()after a user action that implies interest (subscribing to updates, enabling alerts), not on page load. - Handle denial gracefully. Once a user denies permission, you can’t re-prompt programmatically — they have to change it in browser settings. Design for that being a permanent “no” for most users.
- Expect subscriptions to expire or rotate. Push subscriptions can become invalid (browser data cleared, extended inactivity). Handle a failed push send by removing the stale subscription rather than retrying indefinitely.
- Keep payloads small and the service worker fast. The
pushevent has a limited window to callshowNotification()before the browser may terminate the worker.
The takeaway
Web push works by splitting the job across three parties: your service worker to receive and display, the browser vendor’s push service to route messages, and your server to send them — authenticated with a VAPID key pair. That’s what lets a notification reach a user whose tab has been closed for hours, something no ordinary connection-based approach can do. Combine it with the Notifications API for display and treat permission requests as a one-shot ask tied to real user intent, since a denial is effectively permanent.
Tagged
Keep reading
Takina · · 5 min read Promise.all() vs allSettled() vs race() Compared
Promise.all() fails fast, allSettled() waits for every result, and race() returns whichever promise finishes first — how to choose correctly.
Takina · · 4 min read JavaScript Spread vs Rest Operators, Explained
The spread operator (...) expands an iterable into individual elements; the rest operator collects elements back into an array. Same syntax, opposite jobs.
Takina · · 4 min read ResizeObserver API Explained: Watching Element Size
The ResizeObserver API lets JavaScript watch an element's box size and react without polling or resize-event hacks. How it works and when to use it.