Showing Blocked Request Counts on the Badge

Display per-tab blocked counts for an MV3 declarativeNetRequest blocker: displayActionCountAsBadgeText, getMatchedRules and its rate limit, the feedback permission, onRuleMatchedDebug, and accurate popup counts.

Published October 2, 2026 Updated October 2, 2026 7 min read
Table of Contents

Blockers traditionally show a number on their toolbar icon: how many requests were blocked on this page. In MV2 the extension counted them itself in its blocking webRequest listener. In MV3 the browser does the blocking and the extension never sees the requests, so counting needs a different approach. DNR offers three: a one-line option that makes the browser maintain the badge count for you, a query API that reports which rules matched in a tab, and a debug event available only during development. Each has limits that are easy to trip over. This guide covers all three. It belongs to declarativeNetRequest rules.

Three ways to count

chrome.declarativeNetRequest.setExtensionActionOptions({ displayActionCountAsBadgeText: true }) tells the browser to show, on each tab, the number of requests your rules blocked or redirected — without your code doing anything else. It is the simplest and the most efficient, but you cannot read the number or style it beyond the badge colour. chrome.declarativeNetRequest.getMatchedRules({ tabId }) returns the rules that matched in a tab with timestamps, which lets you show counts and details in the popup; it needs either the declarativeNetRequestFeedback permission or an activeTab grant for that tab, and it is rate-limited. onRuleMatchedDebug fires for every match but exists only for unpacked extensions with the feedback permission — a development tool, not a production counter.

Counting options compareddisplayActionCountAsBadgeText, getMatchedRules and onRuleMatchedDebug compared on permissions, availability in production, detail available and rate limits.MethodPermissionProductionDetailLimitsdisplayActionCountAsBadgeTextNone extraYesCount on badge on…NonegetMatchedRulesfeedback or activ…YesRule ids + times20 calls / 10 minonRuleMatchedDebugfeedbackUnpacked onlyEvery matchDev only
Let the browser keep the badge; query details only when the user opens the popup.

Step-by-step: accurate counts without waking the worker

1. Let the browser maintain the badge

1// sw.js
2chrome.runtime.onInstalled.addListener(async () => {
3  await chrome.declarativeNetRequest.setExtensionActionOptions({ displayActionCountAsBadgeText: true });
4  await chrome.action.setBadgeBackgroundColor({ color: "#475569" });
5});

Execution context: the service worker, once on install (the option persists). From then on, each tab’s badge shows how many requests your rules acted on since the tab’s last navigation, with no code running per request. The count resets on navigation automatically. If you also set badge text yourself for a tab, your text takes precedence for that tab — so do not mix both approaches for the same purpose.

2. Offset the count when the user allowlists a site

1export async function onSitePaused(tabId) {
2  // Count continues to reflect actions; on an allowlisted site there should be none after reload
3  await chrome.declarativeNetRequest.setExtensionActionOptions({
4    tabUpdate: { tabId, increment: 0 },
5  });
6  await chrome.tabs.reload(tabId);
7}

Execution context: the service worker. tabUpdate.increment adjusts a tab’s count by a positive or negative amount — useful when cosmetic filtering in a content script hides elements and you want those included, or when you want to subtract something. Most of the time the right move after allowlisting is a reload, which resets the count and lets the new rules apply.

Badge count without extension code per requestRequests on the page are blocked by the browser's DNR matcher, which increments the per-tab badge count itself; only when the user opens the popup does the extension call getMatchedRules for details.PageDNR matcherBadgePopuprequest to trackerblock → count 1request to ad serverblock → count 2getMatchedRules({tabId})[{ruleId, rulesetId, timeStamp}]
The worker sleeps through the page load; it wakes only when the user asks for detail.

3. Query details when the popup opens

1// popup.js
2const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
3const { rulesMatchedInfo } = await chrome.declarativeNetRequest.getMatchedRules({ tabId: tab.id });
4const byRuleset = Object.groupBy(rulesMatchedInfo, (m) => m.rule.rulesetId);
5render({
6  total: rulesMatchedInfo.length,
7  lists: Object.entries(byRuleset).map(([id, ms]) => ({ id, count: ms.length })),
8});

Execution context: the popup. Opening the popup grants activeTab for the current tab, which satisfies getMatchedRules’s permission requirement for that tab without the declarativeNetRequestFeedback permission and its install warning. Grouping by ruleset lets you show “12 ads, 7 trackers” if your rulesets are organised by category. Object.groupBy needs a recent engine; a small reducer works everywhere.

4. Respect the rate limit

1export async function matchedRulesSafe(tabId) {
2  try {
3    return await chrome.declarativeNetRequest.getMatchedRules({ tabId });
4  } catch (err) {
5    if (/quota|rate/i.test(err.message)) return { rulesMatchedInfo: null, rateLimited: true };
6    throw err;
7  }
8}

Execution context: any extension context. getMatchedRules is limited to MAX_GETMATCHEDRULES_CALLS_PER_INTERVAL (20) calls per ten-minute interval when not tied to a user gesture; calls made with activeTab from a user action are generally safe, but polling it from the worker on every navigation will hit the limit within minutes. Never poll; query when the user looks.

Which counting method for this feature?Decision tree: a simple toolbar number uses displayActionCountAsBadgeText; per-list detail in the popup uses getMatchedRules on open; debugging rules during development uses onRuleMatchedDebug.What does the feature need?a number on the icondisplayActionCountAsBadgeTextbrowser-maintainedNo code per requestworker sleepsdetails in popupgetMatchedRulesactiveTab on openNever pollrate-limiteddebugging rulesonRuleMatchedDebugunpacked + feedbackStrip before releasedev builds only
Production counting needs no per-request code at all.

5. Use the debug event only in development

1// sw.js — development builds only
2if (DEV && chrome.declarativeNetRequest.onRuleMatchedDebug) {
3  chrome.declarativeNetRequest.onRuleMatchedDebug.addListener(({ request, rule }) => {
4    console.debug(`[dnr] ${rule.rulesetId}#${rule.ruleId} ${request.type} ${request.url}`);
5  });
6}

Execution context: the service worker of an unpacked extension with declarativeNetRequestFeedback. The event fires for every matched request, which is invaluable for checking new rules and useless in production, where it does not exist. Keep the feedback permission out of production manifests unless you need getMatchedRules without a user gesture — it adds the “Read your browsing history” warning.

6. Keep counts honest

Users treat the badge as a measure of the extension’s value, which tempts inflation. Count what the user would recognise as blocked: requests, not rules evaluated; and do not add cosmetic hides to the network count unless the label says so. If your popup shows a lifetime total (“blocked 1.2 million requests”), compute it from the per-tab counts you read on popup open rather than estimating — or leave it out.

7. Persist per-site summaries if you need history

1// popup.js after getMatchedRules
2const host = new URL(tab.url).hostname;
3const { siteStats = {} } = await chrome.storage.local.get("siteStats");
4siteStats[host] = { last: rulesMatchedInfo.length, at: Date.now() };
5await chrome.storage.local.set({ siteStats });

Execution context: the popup. Matched-rule data is held only for open tabs and recent navigations; if you want “this site usually has 30 trackers”, store a small summary when the user looks. Hostnames are browsing data — keep them local and include them in the privacy policy if you store them.

Common mistakes

  • Polling getMatchedRules. The rate limit stops it within minutes.
  • Requesting declarativeNetRequestFeedback in production for a badge. displayActionCountAsBadgeText needs no extra permission.
  • Setting badge text manually on top of the automatic count. Your text overrides the count for that tab.
  • Shipping onRuleMatchedDebug code paths. The event is undefined in packed builds; guard it.
  • Inflated counts. Users notice, and reviewers may question misleading claims.

Cross-browser variation

  • Chrome / Edge: setExtensionActionOptions, getMatchedRules with activeTab, and onRuleMatchedDebug in unpacked builds.
  • Firefox: supports getMatchedRules and setExtensionActionOptions in recent versions; check support before relying on the automatic badge. Firefox extensions using blocking webRequest can count directly.
  • Safari: DNR counting APIs are limited; compute counts from what you can observe or omit the badge count.

Verification

  1. Enable the badge option, load a page with known trackers, and confirm the badge shows a count that resets on navigation.
  2. Open the popup and confirm the detailed counts match the badge.
  3. Call getMatchedRules more than 20 times in ten minutes from the worker without a gesture and confirm your code handles the rate-limit error.
  4. Allowlist the site, reload, and confirm the count is zero.

FAQ

Does the automatic count include redirects and header modifications?

It counts actions that block or redirect requests; header modifications are not counted.

Can I show the count in the popup without getMatchedRules?

Not exactly — the automatic badge count is not readable. getMatchedRules under activeTab is the way.

Does counting cost performance?

The automatic count is maintained by the browser alongside matching and is negligible. getMatchedRules costs only when called.

Can I change the badge colour when counts are high?

The automatic count uses the colour you set with action.setBadgeBackgroundColor, globally or per tab. Changing colour per tab requires knowing the count, which you only learn through getMatchedRules — so colour by category (for example, red while a security list matched) on popup open rather than by live count.

What happens to counts in incognito?

They work the same way per tab, provided the extension is allowed in incognito. Do not persist incognito site summaries in chrome.storage.local.

Other Core APIs & Cross-Browser Data Management Resources