Web Push Browser Compatibility Reference

This reference maps Web Push support across Chromium (Chrome, Edge), Gecko (Firefox), and WebKit (Safari) so you can decide which features are safe to ship and which need feature detection. It covers minimum versions, per-feature support, the aes128gcm payload contract, and the vendor caveats that break delivery in production.

Web Push is a standard, but a fragmented one. The transport (RFC 8030), encryption (RFC 8291), and VAPID authentication (RFC 8292) are stable, yet each vendor layers its own permission model, payload validation, and UI rendering on top. The matrices below treat the spec as the baseline and call out where each engine diverges.

How Web Push support breaks down by vendor

Three independent push services back the four major browsers. There is no shared infrastructure: a subscription endpoint is opaque, vendor-specific, and bound to both the browser and the applicationServerKey it was created with.

  • Chrome and Edge (Chromium) route through Firebase Cloud Messaging (FCM). They share the same engine, so feature parity is near-total. Edge inherits Chromium behavior including the 4 KB ciphertext ceiling.
  • Firefox (Gecko) uses Mozilla’s autopush service, entirely independent of FCM. Its payload validation is stricter and its feature timeline differs from Chromium’s.
  • Safari (WebKit) routes through Apple Push Notification service (APNs). It is the most constrained: Web Push arrived only in Safari 16 on macOS Ventura and Safari 16.4 on iOS/iPadOS, and on iOS it requires the site to be installed to the Home Screen as a PWA.

Because endpoints are opaque, your server treats all three identically at send time — the same signed request with aes128gcm encryption — and the vendor differences surface as different status codes, size limits, and rendering quirks. The cross-browser notification quirks reference covers the runtime rendering divergences; this page focuses on the version and feature matrix you need before writing a line of code.

Web Push support milestones by vendor A horizontal timeline from 2014 to 2025 marking when Chrome, Firefox and Safari shipped Web Push, VAPID, and notable feature additions. 2014 2025 Chrome 42 (2015) First Web Push Firefox 44 (2016) autopush ships VAPID (2017) RFC 8292 adopted Safari 16 (2022) macOS Web Push iOS 16.4 (2023) PWA-only push Baseline today: Push API + VAPID + aes128gcm on all four engines Transport and encryption converged; rendering options and permission rules did not. iOS remains the outlier: no push from a normal Safari tab.
Web Push milestones: Chrome led in 2015, Firefox followed in 2016, VAPID standardized auth in 2017, and Safari/WebKit closed the gap on macOS in 2022 and iOS in 2023.

Version support matrix

The minimum versions below are the first stable releases where the Push API, Notifications API, and service worker push delivery worked together end to end. Modern evergreen browsers are far past these, but the iOS constraints remain the dominant gotcha.

Browser / Engine Push API support since Push service Platform constraints
Chrome (Chromium) Chrome 42 (2015), VAPID since Chrome 52 FCM Desktop + Android; no iOS engine (uses WebKit there)
Edge (Chromium) Edge 17 (2018, Chromium-based) FCM Desktop + Android; matches Chrome behavior
Firefox (Gecko) Firefox 44 (2016) Mozilla autopush Desktop + Android; not available on iOS
Safari (WebKit) — macOS Safari 16 / macOS Ventura 13.0 (2022) APNs macOS only; standard Web Push API
Safari (WebKit) — iOS/iPadOS Safari 16.4 / iOS 16.4 (2023) APNs Requires install to Home Screen (PWA); no push from a normal tab
Opera / Samsung Internet Tracks Chromium baseline FCM Inherit Chromium feature set and limits

On iOS, a regular Safari tab cannot subscribe at all. navigator.serviceWorker exists, but Notification.requestPermission() and pushManager.subscribe() only function once the user adds the site to the Home Screen and launches it in standalone mode. Detecting window.navigator.standalone (or the display-mode: standalone media query) before offering a prompt avoids a guaranteed failure. This interacts directly with service worker registration patterns, since the worker must already be registered inside the installed PWA context.

Feature support matrix

The transport and encryption are uniform, but the Notification rendering options diverge sharply. Use this table to decide what to send and what to feature-detect at display time inside the service worker.

Feature Chrome / Edge Firefox Safari (macOS) Safari (iOS PWA)
Push API + service worker push Yes Yes Yes Yes (installed PWA only)
VAPID (RFC 8292) auth Yes Yes Yes Yes
aes128gcm content-encoding (RFC 8291) Yes Yes Yes Yes
Max encrypted payload ~4 KB (4096 B) ~4 KB (stricter validation) ~4 KB ~4 KB
Notification actions (buttons) Yes Firefox 152+ (2025) No No
image (large hero image) Yes No No No
badge (monochrome status icon) Yes (Android) Partial No App icon badge only
requireInteraction Yes Yes Ignored (system-managed) Ignored
silent notification Yes (budget-limited) Yes No No
Declarative Web Push (Notification from header) Experimental / origin-trial era No Safari 18.x adopting Safari 18.x adopting
Silent push (userVisibleOnly: false) Restricted (push budget) Restricted No No

Two rows deserve emphasis. First, actions are not portable: Safari ignores them entirely and Firefox only added them in version 152, so any flow that depends on a button must degrade to a plain body and a notificationclick default action — the version thresholds and the runtime Notification.maxActions probe are broken down in which browsers support web push notification actions. Second, silent push is effectively unavailable in the cross-browser sense — every vendor expects a user-visible notification per push, and Chromium enforces this through a push budget that can suppress notifications if you abuse userVisibleOnly: false.

Declarative Web Push

Declarative Web Push lets the push service render a notification from a JSON Notification field in the message without waking your service worker’s push handler. WebKit has driven this proposal and Safari 18.x began adopting it; Chromium experimented with it during an origin-trial period. It is not yet a safe baseline — always ship a push event handler that calls showNotification() as the portable path, and treat declarative rendering as progressive enhancement.

Per-vendor caveats

Chrome and Edge (Chromium / FCM)

Chromium is the most permissive engine but enforces a 4 KB ciphertext limit and a notification-display budget. If you repeatedly send pushes without showing a user-visible notification, the browser revokes your ability to send silent pushes for that origin. Subscriptions can also be revoked when the user clears site data, returning 410 Gone on the next send. Edge behaves identically because it is Chromium under the hood.

Firefox (Gecko / Mozilla autopush)

Firefox validates payloads more strictly than Chromium and historically rejected malformed aes128gcm records that Chrome tolerated. It added notification actions in Firefox 152 (2025); earlier versions silently drop the actions array rather than erroring. Firefox does not render the large image option, so a media-rich design must fall back to icon only.

Safari and WebKit (APNs)

Safari is the strictest target. On iOS the site must be installed as a PWA before any push subscription is possible, and the permission prompt requires a genuine user gesture. Safari ignores requireInteraction, silent, image, and actions, and the system fully manages dismissal. Because the relay is APNs, the same opaque-endpoint model applies, but Apple historically enforced shorter title rendering and tighter UI than Chromium. Always test on a physical device — the iOS Simulator does not deliver real pushes.

A subtle Safari trait is that the standards-based Web Push API on macOS Ventura and iOS 16.4+ is distinct from the older Safari Push Notifications mechanism that used Apple Developer certificates and a pushPackage. The modern path uses VAPID and standard pushManager.subscribe() exactly like Chromium and Firefox, so you do not need an Apple Developer account for it. If you find older documentation referencing certificate-based safari.pushNotification, it predates the standards alignment and does not apply to the Web Push covered in this matrix.

Reading status codes per vendor

Because the three push services validate independently, the same client bug surfaces as different HTTP responses. A 410 Gone means the subscription is dead and should be deleted regardless of vendor. A 404 Not Found is also a permanently invalid endpoint. A 413 Payload Too Large means you exceeded the 4 KB ciphertext ceiling — Firefox tends to return this sooner than Chromium because of its stricter validation. A 429 Too Many Requests is rate limiting from the push service, and a 403 Forbidden almost always means a VAPID signature mismatch — typically a key that no longer matches the subscription’s applicationServerKey. Treating these uniformly in your sender, with 410/404 triggering subscription cleanup, keeps your subscriber list healthy across all three vendors.

Feature detection and verification

Never assume support from a user-agent string alone for capability gating; detect the actual APIs and, for display-time options, probe what the Notification surface accepts. The snippet below gates subscription on real support, including the iOS standalone requirement.

// Client-side capability gate before offering a push opt-in
function getPushSupport() {
  const hasSW = 'serviceWorker' in navigator;
  const hasPush = 'PushManager' in window;
  const hasNotification = 'Notification' in window;

  // iOS only allows push from an installed (standalone) PWA
  const isIOS = /iP(hone|ad|od)/.test(navigator.platform) ||
    (navigator.userAgent.includes('Mac') && 'ontouchend' in document);
  const isStandalone = window.matchMedia('(display-mode: standalone)').matches ||
    window.navigator.standalone === true;

  return {
    supported: hasSW && hasPush && hasNotification && (!isIOS || isStandalone),
    needsInstall: isIOS && !isStandalone,
  };
}

// Detect which NotificationOptions the current engine honors
function supportsActions() {
  try {
    // maxActions is 0 on engines without action-button support
    return 'maxActions' in Notification && Notification.maxActions > 0;
  } catch {
    return false;
  }
}

const support = getPushSupport();
if (support.needsInstall) {
  // Prompt the user to "Add to Home Screen" instead of requesting permission
} else if (support.supported) {
  // Safe to call Notification.requestPermission() on a user gesture
}
Capability gate before offering a push opt-in Check that serviceWorker, PushManager and Notification exist; if not, hide the opt-in. On iOS or iPadOS WebKit, also require display-mode standalone, otherwise guide the user to add the site to the Home Screen. Only then request permission from inside a click handler. Opt-in moment reached Push APIs present? serviceWorker · PushManager no No push support — hide the opt-in yes iOS or iPadOS WebKit? no engine choice on Apple mobile no Prompt on a user gesture yes display-mode: standalone? installed to the Home Screen no Guide the user to Add to Home Screen yes Safe to call requestPermission() from inside a click handler Never gate on the user-agent string — probe the APIs themselves.
The capability gate as a decision tree: three checks, four outcomes, no user-agent sniffing.

On the server, the request is identical across vendors: a VAPID-signed aes128gcm payload under 4 KB. Always source the keys from the environment — never hardcode the VAPID public key server-side. See VAPID key generation and rotation for the full key lifecycle.

// Server-side: one signed request works for all three push services
const webpush = require('web-push');

webpush.setVapidDetails(
  'mailto:ops@yourdomain.com',
  process.env.VAPID_PUBLIC_KEY,
  process.env.VAPID_PRIVATE_KEY
);

async function send(subscription, payload) {
  const body = JSON.stringify(payload);
  if (Buffer.byteLength(body) > 3500) {
    // Leave headroom under the 4 KB ciphertext ceiling after encryption overhead
    throw new Error('Payload too large; send an identifier and fetch the rest client-side');
  }
  // web-push performs ECDH + HKDF + aes128gcm encryption per RFC 8291
  return webpush.sendNotification(subscription, body, { TTL: 86400 });
}

The 4 KB figure is the encrypted ciphertext limit, not your JSON size — RFC 8291 encryption adds overhead, so budgeting roughly 3.5 KB of plaintext is the safe rule. The mechanics of that encoding and how to debug it live in the Push API payload encryption reference.

A portable design baseline

Notification feature tiers by engine coverage Tier one is the universal core honoured by Chrome, Edge, Firefox and Safari: title, body, icon, tag, badge, TTL and the 4 KB budget. Tier two adds actions, requireInteraction and renotify on Chromium and Firefox 152 or later. Tier three is Chromium only: hero image, silent push and declarative push. Ship the universal core, then enhance upward on detection Tier 3 — Chromium only image hero · silent · declarative (experimental) Chrome · Edge Tier 2 — enhance when detected actions (Firefox 152+) · requireInteraction · renotify Chrome · Edge · Firefox Tier 1 — universal core title · body · icon 192×192 · tag · badge · TTL · 4 KB ciphertext budget Chrome · Edge · Firefox · Safari An unsupported option is silently dropped, never reported — detect before you send.
Feature tiers by engine coverage: the base band is what a single send pipeline can rely on everywhere.

Given the matrix above, a payload and handler design that works everywhere narrows to a small, conservative core. Send a title and body, a single same-origin icon at 192×192, an optional badge for Android, and a tag for coalescing. Treat actions, image, requireInteraction, and silent as enhancements applied only when feature detection confirms support, and never depend on silent push or declarative push as a baseline. On the client, gate the opt-in behind real capability detection and, on iOS, behind standalone-mode detection. On the server, enforce the 4 KB ceiling at send time, source VAPID keys from the environment, and treat 410/404 as cleanup signals. This baseline is dull by design — it is the intersection of what Chrome, Edge, Firefox, and Safari all honor, and it is what keeps a single send pipeline reaching every supported browser without per-vendor branching at the transport layer.

Capability detection across embedded and legacy contexts

The matrices above assume a first-class browser window. A large share of real mobile traffic is not one. In-app browsers — the web views embedded in social, messaging, and email apps — render with the platform engine but run under a different set of rules, and they are where push opt-ins quietly evaporate without ever registering as a denial.

Android WebView has no Push API at all: PushManager is absent from window, so an honest capability gate simply does not render the opt-in. Chrome Custom Tabs are a genuine Chrome instance and do support push, but they share the origin’s storage and permission state with standalone Chrome, so a subscription created inside an app’s custom tab reappears in the user’s normal browser — surprising for teams that assume the two are separate profiles, and a common source of “one person, one endpoint, two entry points” confusion in the subscriber table. On iOS every browser and embedded view is WebKit, and SFSafariViewController-style views cannot subscribe under any circumstances; worse, an embedded view cannot install the site to the Home Screen, so the one remedy iOS offers is unreachable from inside it. The only correct response is to detect the embedded context, suppress the prompt, and offer an “open in your browser” affordance instead.

This is exactly why the gate must be built from capability probes rather than from the user-agent string. Every engine that matters misrepresents itself in the UA: Edge advertises both Chrome and Safari, Samsung Internet advertises Chrome, Chrome on iOS advertises CriOS while running WebKit, and the wv token that once marked an Android WebView is inconsistently present. Privacy features freeze, truncate, or randomise the string outright, and navigator.userAgentData is a Chromium-only API — so the one structured alternative is unavailable in precisely the engines you most need to distinguish. A probe, by contrast, is a direct question to the runtime you are actually executing in.

Probe What it proves What it does not prove
'serviceWorker' in navigator The registration API exists on a secure origin That registration will succeed at your scope
'PushManager' in window The Push API is compiled in That subscribe() will be permitted
Notification.permission The current origin-scoped grant state That a prompt is still available to you
Notification.maxActions > 0 The engine renders action buttons How many the platform will actually display
matchMedia('(display-mode: standalone)') The page runs as an installed app That the user installed it deliberately
navigator.permissions.query({name:'notifications'}) Observable permission state, with change events Any ability to re-open a dismissed dialog
Push availability by browsing context Seven browsing contexts and their push status: Chrome or Edge tabs, Firefox tabs and Safari on macOS support push; a Safari tab on iOS cannot subscribe; an installed iOS PWA can; an Android WebView has no Push API; and a Chrome Custom Tab supports push but shares permission state with standalone Chrome. Where a subscription can actually be created Chrome / Edge tab — desktop, Android FCM endpoint supported Firefox tab — desktop, Android Mozilla autopush endpoint supported Safari tab — macOS 13 Ventura or later APNs endpoint supported Safari tab — iOS / iPadOS install required first cannot subscribe Installed PWA — iOS 16.4 or later APNs endpoint supported Android WebView / in-app browser PushManager absent no Push API Chrome Custom Tab FCM endpoint shares Chrome state A context that cannot subscribe should never see the prompt — a suppressed opt-in is recoverable, a denial is not.
Push availability by browsing context. Three of these seven contexts are invisible in a desktop-only test matrix, and two of them account for most mobile social traffic.

The legacy engine floor

Below the versions in the table, push either does not exist or works through a mechanism that has since been removed. Chrome 42 through 51 could subscribe only with an FCM sender ID declared as gcm_sender_id in the web app manifest; VAPID arrived in Chrome 52 and the sender-ID path is long gone, so those builds should be treated as unsupported rather than as a compatibility target. EdgeHTML-era Edge implemented an incomplete push surface against Microsoft’s own notification service and is out of support entirely; every Edge build you will meet in traffic today is Chromium. Firefox below 44 has no Push API. There is no polyfill for any of this — push requires a browser-resident service worker and a vendor relay, neither of which JavaScript can synthesise — so the correct handling of an old engine is a clean, silent fallback to email or in-app messaging rather than a degraded prompt.

Verifying support on a real device

Every claim in this reference is checkable in about a minute per browser. Run the probe first, because it costs nothing and it catches the majority of surprises:

// Paste into the console of the target browser for a one-line support report.
console.table({
  serviceWorker: 'serviceWorker' in navigator,
  pushManager:   'PushManager' in window,
  notification:  'Notification' in window,
  permission:    window.Notification?.permission ?? 'n/a',
  maxActions:    window.Notification?.maxActions ?? 0,
  standalone:    matchMedia('(display-mode: standalone)').matches
});

On Chrome or Edge desktop every boolean is true and maxActions reports 2. On Firefox before 152, maxActions reports 0 while everything else is true. On a Safari tab on iOS the first three are true, standalone is false, and a subscribe() call still fails — which is the whole reason the standalone check exists. Inside an Android WebView, pushManager is false.

Next, confirm which service is actually behind a live subscription. The endpoint host is the ground truth, and it is the fastest way to catch a test that is silently running against the wrong profile:

# Print the push service host for a subscription endpoint you captured client-side.
node -e 'console.log(new URL(process.argv[1]).host)' "$PUSH_ENDPOINT"

Expect exactly one of three answers:

fcm.googleapis.com                      # Chrome, Edge, Opera, Samsung Internet
updates.push.services.mozilla.com       # Firefox
web.push.apple.com                      # Safari on macOS, installed iOS PWA

Then exercise delivery from the browser’s own tooling. Chrome’s DevTools Application → Service Workers pane has a Push button that dispatches a synthetic payload straight to your push handler, which isolates handler bugs from transport bugs. Firefox exposes the registered worker at about:debugging#/runtime/this-firefox, where Inspect opens a console scoped to the worker and a Push button does the same job. Safari on macOS needs the Develop menu enabled, then Develop → Service Workers to attach an inspector. For iOS you must use a physical device with Settings → Safari → Advanced → Web Inspector switched on, attached to a Mac — the simulator never delivers a real push record. Verify on the installed Home Screen app, not the tab, or you will be testing the one iOS context that cannot subscribe.

Divergence matrix: what breaks and where

The failures below are the ones that survive a green feature matrix, because in almost every case the unsupported option is discarded silently rather than reported.

Symptom Engines affected Cause Fix
Action buttons never render Safari (all), Firefox < 152 actions array dropped without error Probe Notification.maxActions; always ship a default click target
Hero image missing Firefox, Safari image unsupported Carry the visual meaning in title/body; keep icon at 192×192
Notification vanishes too soon Safari requireInteraction ignored, system-managed dismissal Never depend on persistence to convey the message
subscribe() rejects on an iPhone Safari iOS tab Not an installed PWA Detect display-mode: standalone and prompt for install instead
Badge count never updates Desktop Chromium, Firefox, Safari badge is an Android surface Treat badging as an Android-only enhancement
413 Payload Too Large on Firefox only Firefox Stricter record validation at the same ceiling Budget ~3.5 KB of plaintext; see maximum payload size limits
Sends stop working after the user clears site data All Subscription revoked; next send returns 410 Gone Delete the row on 410 and stop sending
Works in Chrome, silent in an app’s in-app browser Android WebView No Push API in that context Hide the opt-in; offer “open in browser”
One person appears twice in the subscriber table Chromium + custom tab, or two profiles Endpoints are per browser profile Reconcile on identity, per deduplicating subscriptions across devices

Two of these are worth alerting on rather than debugging after the fact. Track the subscribe() failure rate segmented by engine — a single-engine spike almost always means a version rolled out with a behaviour change, not that your code regressed everywhere. And track the distribution of endpoint hosts in new subscriptions: a sudden shift in the mix is the earliest signal that a capability gate has started rejecting a whole engine, long before the effect is visible in campaign metrics.

FAQ

Does Web Push work in Safari on iPhone?

Yes, from iOS 16.4 onward, but only after the user adds the site to the Home Screen and opens it as an installed PWA. A regular Safari tab on iOS cannot subscribe to push. Detect standalone mode before offering an opt-in.

Is the 4 KB payload limit the same in every browser?

The ~4 KB ceiling applies to the encrypted aes128gcm ciphertext and is effectively uniform across Chrome, Edge, Firefox, and Safari, though Firefox validates more strictly. Because encryption adds overhead, keep plaintext JSON under roughly 3.5 KB.

Do notification action buttons work cross-browser?

No. Chromium has supported actions for years, Firefox added them in version 152 (2025), and Safari ignores them entirely. Always provide a default notificationclick action and treat buttons as progressive enhancement.

Can I send a silent push that shows no notification?

Not reliably. Chromium permits userVisibleOnly: false only within a push budget and will suppress your pushes if you abuse it; Firefox restricts it; Safari does not support it. Design for a visible notification on every push.

Does VAPID work the same way in all browsers?

Yes. VAPID (RFC 8292) authentication is supported by Chrome, Edge, Firefox, and Safari, and a single signed request reaches all three push services. The applicationServerKey used at subscribe time is bound to the resulting subscription across every vendor.

Back to Core Protocols & Browser Implementation