Notification Permissions and Alert Fatigue
Use chrome.notifications without training users to ignore them — the permission model, OS-level blocking, rate limits you should impose yourself, grouping, and a quiet-by-default settings design.
Table of Contents
A system notification interrupts whatever the user was doing in any application, which makes it the most expensive thing an extension can do to someone’s attention. Declaring "notifications" costs nothing at install — there is no warning for it in Chrome — and nothing in the API stops an extension sending forty in an afternoon. The limits have to be yours. Extensions that get this wrong are not blocked by the browser; they are muted at the operating-system level or uninstalled. This guide is part of notifications, badges and the action API.
Who can silence you
Step-by-step
1. Declare the permission, and understand what it does not do
1{
2 "permissions": ["notifications"]
3}
Execution context: parsed at install. In Chrome, extensions with this permission can create notifications without a runtime prompt. That is convenient and also why restraint matters: the user’s first opportunity to object is often the OS-level “turn off notifications from Chrome” — which silences every extension and every website at once.
2. Check whether you can actually notify
1async function canNotify() {
2 const level = await chrome.notifications.getPermissionLevel(); // "granted" | "denied"
3 return level === "granted";
4}
Execution context: the service worker. denied means the user disabled notifications for this extension in the browser. It cannot detect OS-level muting or Focus modes — a created notification in those states is simply queued or dropped, and the create call still succeeds.
3. Make every notification type opt-in and separately controllable
1const NOTIFY_DEFAULTS = {
2 syncErrors: true, // actionable and rare — on by default
3 newArticles: false, // informational — off by default
4 dailySummary: false,
5};
6
7async function wants(type) {
8 const { notify = {} } = await chrome.storage.sync.get("notify");
9 return { ...NOTIFY_DEFAULTS, ...notify }[type] === true;
10}
Execution context: any extension context. Defaulting informational types to off is the single most effective anti-fatigue measure: the users who turn them on wanted them, and nobody else is interrupted. The options-page controls are covered in accessible form controls for extension settings.
4. Rate-limit yourself
1const LIMITS = { newArticles: { perHour: 1 }, syncErrors: { perHour: 2 } };
2
3async function allowed(type) {
4 const { notifyLog = {} } = await chrome.storage.session.get("notifyLog");
5 const hourAgo = Date.now() - 3600e3;
6 const recent = (notifyLog[type] ?? []).filter((t) => t > hourAgo);
7 if (recent.length >= (LIMITS[type]?.perHour ?? 1)) return false;
8 notifyLog[type] = [...recent, Date.now()];
9 await chrome.storage.session.set({ notifyLog });
10 return true;
11}
Execution context: the service worker. The log lives in storage.session because it must survive worker eviction but has no meaning across a browser restart. Hitting the limit is not an error — the underlying state is still visible in the badge and the popup.
5. Group instead of repeating
Twelve new articles are one notification, not twelve. Reuse a notification id to update it in place rather than stacking.
1async function notifyNewArticles(count) {
2 if (!(await wants("newArticles")) || !(await canNotify()) || !(await allowed("newArticles"))) return;
3 await chrome.notifications.create("new-articles", { // fixed id: replaces the previous one
4 type: "basic",
5 iconUrl: "icons/128.png",
6 title: chrome.i18n.getMessage("notifyNewTitle"),
7 message: chrome.i18n.getMessage("notifyNewBody", [String(count)]),
8 priority: 0,
9 });
10}
Execution context: the service worker. A stable id means the second call updates the existing notification’s text instead of adding a second card. priority: 0 keeps it out of the heads-up display on platforms that support priority; reserve higher priorities for errors the user must act on.
6. Make every notification actionable
A notification that cannot be clicked into something useful is pure interruption.
1chrome.notifications.onClicked.addListener(async (id) => {
2 if (id === "new-articles") {
3 await chrome.tabs.create({ url: chrome.runtime.getURL("pages/dashboard.html?view=new") });
4 }
5 await chrome.notifications.clear(id);
6});
Execution context: the service worker, registered at the top level — a click on a notification wakes the worker. Clearing after handling removes it from the notification centre so it does not linger as a stale claim. Buttons and richer layouts are covered in rich notifications with buttons and images.
Choosing between a notification, a badge and nothing
Most things an extension might announce do not justify an interruption. A useful test is to ask what the user would lose if they only found out the next time they opened the popup.
For new content, usually nothing — a badge count says the same thing without interrupting. For a completed long task the user started and then walked away from — an export, a recording, a large import — a notification is right, because they are waiting for it. For a problem that silently breaks the product — sync failing, sign-in expired — a notification is justified once, and the badge should carry the state afterwards.
Quiet hours complete the picture. Many users are happy to receive a daily summary at 9am and unhappy to receive it at 11pm. If your extension sends anything scheduled, let the user choose the time, and respect the OS Focus modes by not trying to work around them.
Cross-browser variation
- Chrome / Edge:
chrome.notificationswithbasic,image,listandprogresstemplates on most platforms; on macOS, notifications go through the system centre and some templates render asbasic. No runtime prompt for extensions with the permission. - Firefox:
browser.notificationssupports thebasictype only; buttons and list/image templates are not available. The permission likewise needs no prompt. - Safari: extension notifications are limited and go through macOS’s notification system, subject to the containing app’s notification permission — which does prompt. Design Safari builds to work without notifications.
- All three: OS-level muting and Focus modes are invisible to the extension. A
createcall that succeeds does not mean a human saw anything.
Verification
- Trigger the same event twelve times within a minute and confirm exactly one notification appears, with the latest count.
- Check the self-imposed log:
1(await chrome.storage.session.get("notifyLog")).notifyLog;
2// { newArticles: [1789…], syncErrors: [] }
Execution context: the service worker console. More timestamps within an hour than the limit allows means the gate is being bypassed by a second code path.
- Turn the type off in your options page and confirm nothing is shown for that event, while the badge still updates.
- Disable notifications for the extension in browser settings and confirm
getPermissionLevel()reportsdeniedand the extension does not error.
FAQ
Does Chrome limit how many notifications an extension can send?
Not in any way you should rely on. Assume there is no limit and impose your own.
Can I tell if the user dismissed a notification rather than clicking it?
notifications.onClosed fires with byUser: true when dismissed. A high dismiss-to-click ratio for a type is a strong signal it should default to off.
Should a notification play a sound?
Leave sound to the operating system’s defaults. Set silent: true for low-priority types on engines that support it.
Is it acceptable to notify about a new version?
Not as a notification. An update is something the extension did, not something the user needs to act on. If a release changes behaviour the user will notice, show a short “what changed” note the next time they open the popup, and let them dismiss it.
Related
- Rich notifications with buttons and images — making the notifications you do send useful.
- Badge text, colour and count patterns — the quieter alternative.
- Scheduling daily and weekly syncs — timing scheduled summaries.
- Notifications, badges and the action API — the parent guide.