Using Regex Filters Within DNR Limits
Write declarativeNetRequest regexFilter rules that compile: RE2 syntax, isRegexSupported, the 1,000 regex rule limit and memory cap, regexSubstitution redirects, and when urlFilter is the better choice.
Table of Contents
A rule needs to match URLs that a simple pattern cannot express — a path segment of exactly eight hex digits, a query parameter in any position, a family of tracking subdomains. regexFilter handles it, and then the problems begin: updateDynamicRules rejects the rule with “regexFilter is invalid”, a ruleset fails to load because it contains too many regex rules, or a redirect with regexSubstitution produces a malformed URL. DNR regular expressions are deliberately constrained so the browser can evaluate them quickly on every request. This guide shows how to write regex rules that compile, fit, and perform. It belongs to declarativeNetRequest rules.
The constraints behind regex rules
DNR compiles regexFilter with RE2, a regular expression engine that guarantees linear-time matching. RE2 omits features that can cause catastrophic backtracking: lookahead and lookbehind assertions, backreferences, and possessive quantifiers are not supported. Each compiled expression must also fit within a memory limit — large alternations and long counted repetitions exceed it. On top of the per-expression limits, Chrome caps the number of regex rules: 1,000 across all enabled static rulesets and, separately, 1,000 across dynamic and session rules. The browser reports these problems at load or update time rather than at match time, and chrome.declarativeNetRequest.isRegexSupported lets you check an expression in advance.
Step-by-step: regex rules that load
1. Prefer urlFilter and domain conditions where possible
1// Regex not needed: a domain plus path prefix
2{ id: 1, priority: 1, action: { type: "block" },
3 condition: { urlFilter: "||cdn.tracker.example/pixel/", resourceTypes: ["image"] } }
4
5// Regex needed: an exact path shape
6{ id: 2, priority: 1, action: { type: "block" },
7 condition: { regexFilter: "^https://[a-z0-9-]+\\.example\\.net/[0-9a-f]{8}/beacon$", resourceTypes: ["xmlhttprequest", "ping"] } }
Execution context: rule JSON in a static ruleset or a dynamic update. urlFilter with || (domain anchor), | (start/end anchor), * (wildcard) and ^ (separator) covers most matching needs, is cheaper to evaluate, and does not count against the regex limit. requestDomains and initiatorDomains are cheaper still. Reach for regexFilter only when the shape of a path or parameter matters. In JSON, backslashes are escaped, so \. becomes \\..
2. Check expressions before shipping them
1// sw.js or a build script running in Chrome
2export async function validateRegex(regex, caseSensitive = false) {
3 const r = await chrome.declarativeNetRequest.isRegexSupported({ regex, isCaseSensitive: caseSensitive });
4 return r.isSupported ? { ok: true } : { ok: false, reason: r.reason }; // "syntaxError" | "memoryLimitExceeded"
5}
Execution context: an extension context. Run it in a build step (via a headless browser with the extension loaded) over every regex in your rulesets, and in the options page before accepting a user-entered regex. memoryLimitExceeded means the expression is too large once compiled — typically a long alternation or a large repetition count — and must be split or simplified.
3. Rewrite unsupported constructs
1Wanted: block /track?... unless the URL contains "consent=1"
2Regex: ^https://a\.example/track\?(?!.*consent=1) ← lookahead: unsupported
3
4DNR: rule A (priority 1) block regexFilter "^https://a\.example/track\?"
5 rule B (priority 2) allow regexFilter "^https://a\.example/track\?.*consent=1"
Execution context: rule design. Negative conditions become a higher-priority allow rule that carves an exception out of a broader block. Many other conditions RE2 cannot express directly are better expressed with DNR’s own fields — excludedRequestDomains, excludedInitiatorDomains, excludedResourceTypes, excludedRequestMethods — which are also cheaper to evaluate.
4. Keep within the rule-count limits
1export async function regexBudget() {
2 const dyn = await chrome.declarativeNetRequest.getDynamicRules();
3 const ses = await chrome.declarativeNetRequest.getSessionRules();
4 const used = [...dyn, ...ses].filter((r) => r.condition.regexFilter).length;
5 const max = chrome.declarativeNetRequest.MAX_NUMBER_OF_REGEX_RULES; // 1000
6 return { used, remaining: max - used };
7}
Execution context: the service worker. The constant is exposed by the API; static rulesets have their own separate allowance shared across enabled rulesets. Adding a dynamic regex rule beyond the limit fails the whole updateDynamicRules call, so check before adding and give the user a clear message. Consolidate related patterns: one regex with a short alternation ((ads|track|pixel)) can replace three rules, as long as it stays within the memory limit.
5. Redirect with regexSubstitution
1{
2 id: 10, priority: 1,
3 action: { type: "redirect", redirect: { regexSubstitution: "https://\\1.example.org/\\2" } },
4 condition: {
5 regexFilter: "^https://old-([a-z]+)\\.example\\.com/(.*)$",
6 resourceTypes: ["main_frame", "sub_frame"],
7 },
8}
Execution context: a rule in a static or dynamic ruleset. \\0 is the whole match and \\1–\\9 are capture groups. Redirect rules need host permissions for both the original and the target URL, and they count toward the stricter “unsafe rules” allowance in dynamic rules. Test that the substituted URL is valid for every shape the regex can match — a missing group produces an empty segment rather than an error.
6. Make case sensitivity explicit
1condition: { regexFilter: "^https://[^/]+/Download/[^?]+\\.exe$", isUrlFilterCaseSensitive: false }
Execution context: rule JSON. Regex and URL filters are case-insensitive by default in recent Chrome versions; older versions defaulted to case-sensitive. Setting isUrlFilterCaseSensitive explicitly makes the behaviour the same everywhere. Hostnames are lower-cased by the browser before matching, so case only matters in paths and queries.
7. Test regex rules against real URLs
1// unpacked build only — requires declarativeNetRequestFeedback
2const res = await chrome.declarativeNetRequest.testMatchOutcome({
3 url: "https://ab12cd34.example.net/0a1b2c3d/beacon",
4 type: "xmlhttprequest",
5 initiator: "https://news.site",
6});
7console.log(res.matchedRules); // [{ ruleId: 2, rulesetId: "_dynamic" }]
Execution context: the service worker of an unpacked extension with the declarativeNetRequestFeedback permission. testMatchOutcome evaluates a hypothetical request against all enabled rules and returns the rules that would match, without loading anything. Build a table of URLs that must and must not match each regex and run it in CI. More in testing declarativeNetRequest rules offline.
Common mistakes
- Using regex where
urlFiltersuffices. It wastes the scarce regex budget and evaluates more slowly. - Lookaheads. RE2 rejects them; express exceptions as allow rules.
- Unescaped dots and slashes in JSON.
example.comin a regex matchesexampleXcom; escape asexample\\.com. - Large alternations. Hundreds of alternatives in one regex exceed the memory limit; use
requestDomainslists instead. - Accepting user regexes without validation. One bad rule fails the whole dynamic update.
Cross-browser variation
- Chrome / Edge: RE2 semantics,
isRegexSupported, 1,000-rule regex limits,testMatchOutcomein unpacked builds. - Firefox: supports
regexFilterwith its own regex engine constraints and limits;isRegexSupportedis available. Test rules in Firefox separately. - Safari: supports
regexFilterwith a more limited syntax; some expressions valid in Chrome are rejected. PreferurlFilterand domain conditions for portable rulesets.
Verification
- Run
isRegexSupportedon every regex in your rulesets during the build; fail on any unsupported expression. - Load the extension and confirm no ruleset load errors on
chrome://extensions. - Use
testMatchOutcome(unpacked) with must-match and must-not-match URLs for each regex rule. - Add dynamic regex rules up to the limit in a test profile and confirm your budget check prevents the next one.
FAQ
Are regex rules slower?
They are evaluated only for requests that pass cheaper conditions such as requestDomains and resourceTypes, so pair every regex with those to narrow the candidates.
Can I use regexFilter with requestDomains together?
Yes, and you should — the domain condition filters first, and the regex runs only on matching domains.
Why did a rule match in Chrome but not Firefox?
Regex engines and defaults differ. Test portable rulesets in each engine, and favour urlFilter for rules that must behave identically.
How do I migrate MV2 regex logic that used lookaheads?
List each pattern’s intent in plain words first — “block X except when Y” — and express the exception as a separate, higher-priority allow rule or an excluded… condition. Most MV2 lookaheads were exceptions in disguise and translate cleanly once written that way.
Related
- Debugging rules that don’t match — when a regex looks right but never fires.
- Redirecting and rewriting headers with rules — redirects beyond regexSubstitution.
- Staying under the rule count limits — the overall budget.
- declarativeNetRequest rules — the parent topic.