One misplaced quantifier inside one capture group can take down the API on a Tuesday afternoon. Catastrophic backtracking is a regex failure mode where a pattern looks fine in tests, runs in microseconds on the developer’s laptop, and then spikes to multi-minute CPU on production data because a single long input forces the engine to explore an exponential number of paths through the backtracking graph. The Regex Linter is built to flag that family — and three more anti-pattern families — before the pattern ever reaches a request handler. This guide walks through the eight things it checks, why each one matters, and how to read its rewrite suggestions.

Why regex can lock up a server: backtracking, the basic case
A regex engine that supports backtracking runs in linear time on well-behaved patterns and exponential time on adversarial ones. The classic example is (a+)+ against a long string of as followed by a single non-a: the engine sees the outer group, captures as, tries to match the inner a+ again, fails, backtracks one capture, tries again — and at every failure the inner group re-evaluates from scratch. The result is that matching a 30-character string takes billions of steps, and a 60-character string takes longer than the rest of the request budget combined.
This is not a rare problem. The Cloudflare ReDoS writeup catalogues 143 production patterns that exhibited the same shape — nested quantifiers, alternating wildcards, overlapped alternations. A 2019 paper measured 467 ReDoS-prone regexes in the npm registry alone. The pattern slips into frameworks via copy-paste from Stack Overflow, and once it lands in a request validator or a routing matcher it stays until somebody notices a latency spike.
The four severity bands the linter reports
The Regex Linter classifies findings into four bands, ordered by impact:
- Critical — patterns the engine may evaluate in super-linear time. Nested quantifiers, ambiguous alternations, unbounded repetitions on hot paths.
- Error — patterns that match the wrong substring on at least some inputs. Unescaped dots outside intended literals, character-class ranges like
[a-Z]that include unexpected bytes, missing anchors on validation rules. - Warning — patterns that work but confuse the next reader. Observable non-greedy, double negation, redundant backslashes, ambiguous alternation ordering.
- Info — patterns that work and are correct but verbose. Capturing groups used as non-capturing, verbose quantifiers like
dddinstead ofd{3}.
The recommendation from the tool itself is to run <strong>standard</strong> in CI and <strong>strict</strong> before merging a new pattern into a shared library — basic is for hot-path CI where any latency from the linter itself is unwelcome. Each band has a per-rule description, a concrete test case, and a rewrite suggestion. Three bands worth digging into next.
Critical: catastrophic backtracking and how to defuse it
The catastrophic-backtracking rule fires on patterns that match the ReDoS shape. The signature is one of three:

- Nested quantifier on an overlapping group — patterns like
(a+)+,(.),([a-z]+)+. The inner repetition and the outer repetition share characters; the engine explores every split. - Adjacent overlapping repetitions — patterns like
a+aor\d+\d+. The two quantifiers can each own the boundary, so the engine tries every split point. - Alternation with overlapping branches — patterns like
a|a+where one branch is a prefix of the next.
The fix is mechanical but depends on the shape of the input:
- Use a possessive quantifier or atomic group (when the engine supports it):
(?:[a-z]+)+becomes[a-z]++in PCRE / Java, or(?>[a-z]+)+as the portable atomic-group form. The engine commits the first capture without backtracking. - Use a negated character class:
[^a]*ais linear;(a+)+is exponential. Rewriting the inner quantifier to “any character that is not the boundary” cuts the alternatives to one. - Use the RE2 engine if you can (Go’s
regexp, re2 library in Python). RE2 refuses to backtrack; it always runs in linear time at the cost of dropping backreferences and lookaround. If you can use it, every catastrophic-backtracking finding disappears.
The Regex Linter emits one of these three rewrite suggestions automatically when it flags the pattern. The “strict” check level also adds a warning when the pattern relies on a backreference — RE2 will not run it.
Error: unescaped dots and the “matches anywhere” trap
The next band catches patterns that look right to the author and match the wrong substring on real input. Two patterns recur:
- Unescaped dots outside literal context —
2026.01.15looks like a date matcher, but the dots are wildcard metacharacters; it matches 2026X01X15 and 202600115 equally well. The fix is2026.01.15, or use[.]as the portable form that survives dot-all modes. - Character-class ranges that include unexpected codepoints —
[a-Z]is technically valid and matchesatoZin ASCII, but in between it sweeps in<code>[</code>,,<code>]</code>,^,<code>_</code>, and</code>. Most validators intended "letters" want[a-zA-Z], not the half-range.
The linter also flags octal escapes like