Skip to main content

Fixing PWA Notification Badges Not Updating in Microsoft Edge on Windows 11

 You have built a Progressive Web App, implemented the necessary background syncing, and deployed your notification logic. On macOS with Chrome, the dock icon updates perfectly. However, when users install your application via Microsoft Edge, the badge counter refuses to appear on the Windows 11 taskbar.

Calling navigator.setAppBadge() seems to fail silently. There are no console errors, the Service Worker is active, and push messages arrive on time.

This is a known architectural quirk regarding how Microsoft Edge handles Progressive Web App desktop integration with the Windows OS shell. Resolving this requires modifying how your Service Worker handles background execution lifecycles and strictly managing API promises.

The Root Cause: The Edge and Windows Shell Bridge

When a user installs a PWA via Microsoft Edge on Windows 11, the browser does not merely create a shortcut. It generates a lightweight Appx package under the hood. This package binds the web application directly to the Windows Shell, enabling native features like taskbar pinning, uninstall menus, and badge notifications.

The JavaScript AppBadging API must cross a bridge from the V8 engine, through Edge's background processes, into the Windows Notification Service (WNS). If a badge update is fired but the Service Worker terminates before Edge can pass the instruction to the Windows Shell, the update is permanently dropped.

Furthermore, Edge strictly throttles background execution. If you fire a synchronous badge update inside a Push API event handler without explicitly extending the Service Worker's lifecycle, the worker will suspend before the asynchronous setAppBadge operation completes its trip to the Windows taskbar.

The Fix: Lifecycle-Aware Badge Management

To guarantee that the Edge PWA badging API correctly syncs with the Windows 11 taskbar, we must wrap all badge updates in unresolved Promises during background events. We also need to synchronize the badge state seamlessly when the application regains foreground visibility.

1. The Safe Badge Wrapper Utility

First, establish a robust utility function. The App Badging API is inherently asynchronous. Failing to await it is the primary reason the bridge to the OS shell fails.

Create a utility module to handle feature detection and asynchronous execution securely.

// utils/badging.ts
export async function updateAppBadge(count: number): Promise<void> {
  if ('setAppBadge' in navigator) {
    try {
      // Must be awaited to ensure the OS integration layer processes the request
      await navigator.setAppBadge(count);
    } catch (error) {
      console.error('Failed to update Windows taskbar badge:', error);
    }
  }
}

export async function clearAppBadge(): Promise<void> {
  if ('clearAppBadge' in navigator) {
    try {
      await navigator.clearAppBadge();
    } catch (error) {
      console.error('Failed to clear Windows taskbar badge:', error);
    }
  }
}

2. Extending the Service Worker Lifecycle

The most critical fix occurs in your sw.js (Service Worker) file. When a background push arrives, the Service Worker wakes up momentarily.

You must use event.waitUntil() to freeze the worker's termination clock until the badge operation successfully communicates with the Windows Shell.

// sw.js
self.addEventListener('push', (event) => {
  if (!event.data) return;

  try {
    const payload = event.data.json();
    const unreadCount = payload.unreadCount ?? 1;

    // Extend the SW lifecycle until both notification and badging promises resolve
    event.waitUntil(
      Promise.all([
        self.registration.showNotification(payload.title, {
          body: payload.message,
          icon: '/icon-192x192.png',
        }),
        updateBadgeInServiceWorker(unreadCount)
      ])
    );
  } catch (error) {
    console.error('Push event processing failed:', error);
  }
});

async function updateBadgeInServiceWorker(count) {
  if ('setAppBadge' in navigator) {
    // The await here ensures the promise returned to waitUntil() 
    // stays pending until Edge confirms the OS-level badge update.
    await navigator.setAppBadge(count);
  }
}

3. Foreground State Synchronization

When the user clicks the taskbar icon to bring the PWA to the foreground, the badge should clear immediately. Windows 11 handles taskbar interactions at the OS level, but clearing the badge is strictly the responsibility of your frontend JavaScript.

Bind the clearance logic to the Document Visibility API to ensure the taskbar updates the exact millisecond the user views the app.

// components/AppLifecycleManager.tsx
import { useEffect } from 'react';
import { clearAppBadge } from '../utils/badging';

export function AppLifecycleManager() {
  useEffect(() => {
    const handleVisibilityChange = async () => {
      if (document.visibilityState === 'visible') {
        // Clear the badge immediately when the user focuses the Edge PWA
        await clearAppBadge();
        
        // Optional: Ping your backend to reset the unread count in your database
        // await fetch('/api/notifications/mark-read', { method: 'POST' });
      }
    };

    document.addEventListener('visibilitychange', handleVisibilityChange);
    
    // Run once on mount in case the app loads directly into the foreground
    handleVisibilityChange();

    return () => {
      document.removeEventListener('visibilitychange', handleVisibilityChange);
    };
  }, []);

  return null;
}

Why This Architecture Works

By grouping navigator.setAppBadge() alongside showNotification inside Promise.all(), you force the Microsoft Edge background process to stay alive. The event.waitUntil() method dictates the exact lifecycle of the Service Worker.

If you omit the await on the badging API, Promise.all resolves immediately after the notification is triggered. Edge then signals Windows that the Service Worker is idle. Windows aggressively suspends the worker thread to save battery, halting the setAppBadge execution mid-flight.

Wrapping the visibility event listener in an asynchronous handler guarantees that foreground badge clearances do not block main-thread rendering. It creates a smooth UX where the taskbar indicator vanishes instantly upon window focus.

Managing Windows 11 OS-Level Edge Cases

Even with perfect code, Windows 11 PWA notifications and badging can fail due to user configuration. You must account for OS-level settings that override browser APIs.

Taskbar Badging Settings

Windows 11 allows users to globally disable taskbar badges. If a user navigates to Settings > Personalization > Taskbar > Taskbar behaviors and unchecks "Show badges on taskbar apps", your API calls will resolve successfully in JavaScript, but no visual badge will appear.

Currently, the App Badging API provides no mechanism to detect if OS-level badging is disabled. Your code should always assume the update was successful to prevent unnecessary retry loops.

Notification Focus Assist (Do Not Disturb)

If Windows 11 is in "Do Not Disturb" mode, Edge may silently defer background syncing for installed PWAs. This can delay Push API events entirely. Always rely on a combination of push events and foreground fetch polling. If a user opens the app without receiving the push, your foreground data-fetching logic should reconcile the local application state and manually call navigator.setAppBadge() if necessary.