How Does Web Push Actually Work? Service Workers, VAPID, and Push Services Explained
Web push involves three parties: your web page, a push service run by the browser vendor, and your own server. The page registers a service worker and asks the browser for a push subscription — a unique URL on the vendor's push service plus a pair of encryption keys. To send a notification, your server posts an encrypted, VAPID-signed message to that URL, and the push service wakes the service worker on the user's device, which displays the notification even when the site is not open. No polling, no persistent connection from your page — the browser and the operating system do the waiting for you.
That is the whole model in one paragraph. The rest of this post walks through each piece — the service worker, the subscription, VAPID, encryption — and the platform realities on iOS and Android that the specs politely leave out.
Who are the three parties in web push?
It helps to name the actors precisely, because two of them are easy to conflate:
- Your page and its service worker. The page requests permission and creates the subscription. The service worker is the piece that stays registered after the tab closes and handles incoming pushes.
- The push service. Operated by the browser vendor, not by you. Chrome subscriptions point at Google's push infrastructure, Firefox at Mozilla's, Safari at Apple's. You never choose it and you never run it — the subscription endpoint the browser hands you simply lives on that vendor's servers.
- Your application server. The only part you own. It stores subscriptions and sends messages to them using the Web Push protocol.
The crucial consequence: your server never talks to the user's device directly. It talks to the push service, and the push service maintains the long-lived connection to the device. That is why push works when your site is closed — the connection belongs to the browser and the OS, not to your page.
What does the service worker actually do?
Two things: it is the subscription anchor, and it is the code that runs when a push arrives. Subscribing looks like this on the page:
const reg = await navigator.serviceWorker.register('/sw.js');
const sub = await reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: VAPID_PUBLIC_KEY // Uint8Array
});
// Send sub.toJSON() to your server and store it.
The resulting subscription object contains the endpoint URL and two client keys (p256dh and auth) used for payload encryption. Your server stores all three.
Inside the service worker, the push event fires when the push service delivers a message. The handler is expected to show a notification — browsers enforce userVisibleOnly, so silent pushes are not an option:
self.addEventListener('push', (event) => {
const data = event.data ? event.data.json() : {};
event.waitUntil(
self.registration.showNotification(data.title || 'Update', {
body: data.body || '',
data: { url: data.url || '/' }
})
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
event.waitUntil(clients.openWindow(event.notification.data.url));
});
The event.waitUntil() call matters: it keeps the service worker alive until the notification is shown. Without it, the browser may terminate the worker mid-handler, and some browsers show a generic "this site has been updated in the background" notification instead of yours.
What is VAPID and why do you need keys?
VAPID (Voluntary Application Server Identification, defined in RFC 8292) is how the push service knows that the party sending a message is the same party the user subscribed to. You generate a public/private key pair once. The public key goes into pushManager.subscribe() as the applicationServerKey; the push service records it with the subscription. Every message you send carries a short-lived JWT signed with the private key, and the push service checks the signature against the recorded public key before accepting the message.
Two practical notes. First, no Firebase account, SDK, or vendor registration is required — VAPID replaced the old per-vendor sender-ID schemes, so the same key pair works against Google's, Mozilla's, and Apple's push services. Second, treat the private key like any other credential: anyone holding it can push to every subscription created against its public key, and rotating it invalidates those subscriptions, forcing every client to resubscribe.
How is the payload encrypted?
Push payloads are end-to-end encrypted between your server and the browser. Your server encrypts each message against the subscription's p256dh and auth keys, so the push service relays bytes it cannot read. This is not optional — browsers reject unencrypted payloads. In practice you never hand-roll this: mature Web Push libraries exist for most server languages, and they handle the encryption, the VAPID JWT, and the delivery request in one call. The MDN Push API documentation is the best reference if you want the details underneath.
Does web push work on iPhone and Android?
This is where theory meets platform policy, and it is the question most tutorials dodge.
Android is the easy case. Chrome, Firefox, Edge, and Samsung Internet all support web push from the regular browser, and the OS wakes the browser to deliver notifications even when it is not running. A user can subscribe on a normal website in a normal tab.
iOS supports web push since iOS 16.4 — but only for web apps added to the Home Screen. A site visited in a Safari tab cannot subscribe; the user must first install it (Share, then "Add to Home Screen"), open it from the Home Screen icon, and grant permission from a user gesture inside that installed app. Once granted, notifications arrive in the normal iOS notification center with sound, badges, and lock-screen display, like any native app. The practical consequence for product design: on iOS you must guide users through installation before you ever mention notifications.
Desktop support is broad — Chrome, Edge, and Firefox everywhere, Safari on macOS. The caveat is that a desktop browser generally needs to be running (foreground or background) to receive pushes, whereas mobile platforms wake the handler for you.
How should you ask for notification permission?
Never on page load. Browsers have responded to prompt spam aggressively: Chrome and Firefox quiet or suppress permission prompts on sites users routinely dismiss, and a user who hits "Block" is essentially gone — resetting a blocked permission is buried deep enough in browser settings that almost nobody does it.
The pattern that works is a two-step ask. First show your own in-page UI — a button or card explaining what the notifications are for ("Get an alert when your monitor goes down"). Only when the user clicks it do you call the real browser prompt. A user who has just expressed intent almost always accepts, and a user who ignores your in-page card has cost you nothing: the browser-level permission is still unspent, so you can ask again another day.
How reliable is web push?
Web push is best-effort, and honest system design starts from that. The delivery protocol (RFC 8030) lets senders set a TTL — how long the push service should hold a message for an offline device — and an urgency hint, but no push service promises delivery or latency. Devices go offline past the TTL, vendors throttle, operating systems defer background work to save battery. Subscriptions also die: they expire or are revoked, and your server learns this only when a send returns 404 or 410, at which point you should delete the stored subscription.
The design consequence: push should be the tap on the shoulder, not the system of record. Keep the authoritative copy of every alert somewhere durable the user can open later — a chat thread, a log, an inbox — so a dropped push means a late read, not a lost message. This is exactly how we approach uptime alerts: the message exists in a real-time chat first, and the push is a pointer to it.
Do you have to build all of this yourself?
Only if you want to. The full stack — service worker, subscription storage, VAPID key management, payload encryption, expired-subscription cleanup, the iOS install dance — is a real project, and it is worth building when notifications are your product's core surface.
When you just need a message to reach your own phone — a cron job finished, a deploy failed, a backup did not run — you can skip the entire stack. Pingwire already runs it: the PWA handles subscriptions and the service worker, and every message lands in a real-time chat and as a web push on your subscribed devices. Sending is one HTTP call or one CLI command. If that is your use case, start with sending a push from a bash script, or let your AI agent send the ping when a long-running task completes.
Frequently asked questions
Does web push work when the browser is closed?
On Android, yes — the operating system wakes the browser's service worker to handle a push even when the browser app is not open. On desktop, the browser generally must be running, at least in the background. On iOS, installed Home Screen web apps receive pushes through the system like native apps.
Does web push work on iPhone?
Yes, since iOS 16.4, but only for web apps the user has added to their Home Screen. A site open in a regular Safari tab cannot subscribe. After installation and a permission grant from a user gesture, notifications arrive in the normal iOS notification center.
Do I need Firebase to send web push notifications?
No. Web push is an open standard. Chrome subscriptions point at Google's push infrastructure, but you talk to it with the standard Web Push protocol and VAPID keys — no Firebase SDK or account is required, and the same code works against Mozilla's and Apple's push services.
What are VAPID keys?
A public/private key pair identifying your application server to push services. The public key is supplied when the user subscribes; the private key signs a short-lived token on every send, proving to the push service that the sender is the party the user subscribed to.
Is web push guaranteed to be delivered?
No. Push services may throttle, delay, or drop messages, and a device that stays offline past the message's TTL never receives it. Treat push as best-effort: keep the authoritative copy of every alert somewhere durable, and use the push as a pointer to it.
Try Pingwire
Send your first alert in under 30 seconds — one HTTP call, straight to a chat and your phone.