Practical guide 6 min read

Greedy, Lazy, and Catastrophic: The Three Regex Behaviors That Explain Most Regex Bugs

Why .* overmatches, when lazy quantifiers save you, and how to spot the backtracking patterns that can hang your production server.

Most regex bugs reduce to three behaviors: greediness, laziness, and catastrophic backtracking. Understand these three and the regex chapter of your debugging life gets dramatically shorter.

Greedy quantifiers take everything they can

<.*> applied to <a><b> does not match <a> — it matches the entire <a><b>, because * grabs as much as possible and only backtracks if forced. Greedy is the default for *, +, ?, and {m,n}. This is the number one “why did it match more than I expected” complaint, and it is not a bug: it is the documented behavior.

Lazy quantifiers stop at the first opportunity

Appending ? inverts the strategy: <.*?> on <a><b> matches <a>, because .*? expands only as far as needed for the rest of the pattern to fit. Common lazy forms worth memorizing: .*? (anything, minimal), .+? (something, minimal), \d{2,4}? (as few digits as possible).

The alternative to laziness is often better, though: a negated character class. <[^>]*> matches up to the first > without any backtracking at all — it is both clearer in intent and faster in execution. Reach for [^…] first, laziness second.

Catastrophic backtracking: the bug that hangs servers

Nested quantifiers create exponential backtracking. The notorious pattern:

^(a+)+$

Against aaaaaaaaaaaaaaaaaaaaaaaa!, the engine tries exponentially many ways to split the a-run before concluding there is no match. Matching time doubles with every extra character — a 30-character input can take minutes, a 40-character input hours. Attackers know this: sending crafted inputs to any endpoint that runs user-supplied regexes (search, validation, log filters) is a denial-of-service technique called ReDoS. Real incidents have taken down whole platforms through a single vulnerable pattern in a request validator.

Patterns at risk share a shape: quantified groups where the inner and outer quantifiers can match the same text — (a+)+, (\d|\w)+, (a|aa)+. If your regex contains nested quantifiers, treat it as guilty until proven innocent.

The four-step defense

  1. Test with a long adversarial input — 30–50 characters of the repeated token followed by a character that forces failure. If the test hangs, the pattern is vulnerable.
  2. Prefer negated classes and anchors^[^,]+, cannot backtrack exponentially the way ^(.+)+, can.
  3. Use atomic groups or possessive quantifiers where available(?>a+) or a++ commit to their match and never backtrack (PCRE, Java, .NET; JavaScript is gaining them in newer engines).
  4. Bound the input before regex runs — a length limit converts a ReDoS from “server down” to “400 response”.

The Regex Tester shows match-by-match results and makes adversarial testing a two-minute routine: paste the pattern, paste a long hostile sample, watch whether it returns instantly. And when a regex still behaves mysteriously, a text diff between the matched and expected output usually reveals the invisible characters behind the surprise.