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.

Published October 2, 2026 Updated October 2, 2026 7 min read
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.

Regex features in DNRWhich regular expression features RE2 supports in declarativeNetRequest regexFilter, with an alternative for each unsupported feature.FeatureSupportedAlternativeCharacter classes, anchorsYes—Groups and alternationYesKeep alternations shortCounted repetition {n,m}Yes, small countsAvoid {1,1000}Lookahead / lookbehindNoTwo rules, or excludedRequestDomainsBackreferencesNoSeparate rulesCase-insensitiveisUrlFilterCaseSensitive: false—
If you need a lookahead, you probably need two rules or a urlFilter.

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.

Validating a user-entered regex ruleThe options page sends the regex to the worker, which checks syntax and memory with isRegexSupported, checks the remaining regex rule budget, and only then adds the dynamic rule.User enters regexoptions pageisRegexSupportedsyntax + memoryBudget check< 1,000 regex rulesboth passupdateDynamicRulesadd ruleStore metadatalabel, createdShow in listwith test URL
Two checks — the expression and the budget — before the browser ever sees the rule.

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.

DNR rule limits that affect regex rulesRegex rule limits for static and dynamic plus session rules, compared with overall dynamic and guaranteed static rule limits in Chrome.Regex rules (static)1000 rulesRegex rules (dynamic + session)1000 rulesSession rules5000 rulesDynamic rules30000 rulesGuaranteed static rules30000 rules
Regex rules are the scarcest kind — budget them separately.

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 urlFilter suffices. 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.com in a regex matches exampleXcom; escape as example\\.com.
  • Large alternations. Hundreds of alternatives in one regex exceed the memory limit; use requestDomains lists 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, testMatchOutcome in unpacked builds.
  • Firefox: supports regexFilter with its own regex engine constraints and limits; isRegexSupported is available. Test rules in Firefox separately.
  • Safari: supports regexFilter with a more limited syntax; some expressions valid in Chrome are rejected. Prefer urlFilter and domain conditions for portable rulesets.

Verification

  1. Run isRegexSupported on every regex in your rulesets during the build; fail on any unsupported expression.
  2. Load the extension and confirm no ruleset load errors on chrome://extensions.
  3. Use testMatchOutcome (unpacked) with must-match and must-not-match URLs for each regex rule.
  4. 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.

Other Core APIs & Cross-Browser Data Management Resources