toolhq.io

All posts
6 min readby Jameel Haider

Greedy vs lazy quantifiers: why your regex matches too much

The most common regex complaint is that a pattern matched far more than intended. Given <b>bold</b> and <i>italic</i>, the pattern <.*> matches the entire string rather than the first tag. Nothing is broken: quantifiers are greedy by default, meaning they consume as much as possible and give characters back only when the rest of the pattern cannot otherwise match.

You can watch this happen against your own input in the Regex Tester, which highlights matches live as you type.

How greedy matching works

Given <.*> and the input above, the engine does this:

  1. < matches the first character.
  2. .* consumes everything to the end of the string.
  3. > has nothing left to match, so the engine backtracks: .* gives back one character at a time.
  4. It gives back until the character after .* is the final >, which succeeds.

The result is <b>bold</b> and <i>italic</i>, one match spanning the whole line. Greedy quantifiers always try the longest possibility first and shrink only under pressure.

Lazy quantifiers

Appending ? to a quantifier inverts the preference. A lazy quantifier consumes as little as possible and expands only when forced:

GreedyLazyMeaning
**?Zero or more
++?One or more
???Zero or one
{2,5}{2,5}?Between two and five

With <.*?> the engine starts with .*? matching nothing, tries > against b, fails, expands by one character, and repeats until > matches. The result is <b>, then </b>, then <i>, and so on: four separate matches, which is what was wanted.

The rule of thumb: when extracting something delimited, quoted strings, tags, bracketed sections, reach for lazy. When matching to the end of something, greedy is correct.

Input:   key = "first" and "second"

"[^"]*"   →  "first"    "second"     best
".*?"     →  "first"    "second"     fine
".*"      →  "first" and "second"    wrong

The better option: a negated character class

Notice the first line in that comparison. "[^"]*" says "a quote, then any number of characters that are not quotes, then a quote", which describes the intent directly rather than relying on backtracking to discover it. It is usually clearer and always faster, because there is nothing to backtrack: the class simply cannot cross the delimiter.

For the tag example, <[^>]*> is the better form of <.*?>. The lazy version still works by trial and expansion; the negated class version marches forward once.

Lazy quantifiers also have a subtlety that catches people. <.*?> applied to <a href="x>y"> matches <a href="x> because it stops at the first > it finds, even one inside quotes. Lazy means shortest match from the current start position, not "the correct one". When the delimiter can appear inside the content, neither greedy nor lazy is sufficient and you need a more explicit pattern or, for structured formats, a real parser. This is the same reason regex is the wrong tool for HTML and general nesting.

Possessive quantifiers and atomic groups

Some engines (PCRE, Java, Ruby, and .NET via atomic groups) offer a third mode: possessive. .*+ consumes as much as possible and refuses to give anything back. If the rest of the pattern then fails, the whole match fails rather than backtracking.

That sounds unhelpful until you consider performance. Possessive quantifiers and atomic groups (?>...) are how you tell the engine that backtracking into a section can never produce a match, which lets it abandon dead ends immediately. JavaScript supports neither, though the same effect is sometimes achievable with a lookahead capturing group.

Catastrophic backtracking

Backtracking is also where regular expressions become a denial of service risk. Consider:

^(a+)+$

against the input aaaaaaaaaaaaaaaaaaaaX. There is no match, but the engine must prove it, and the nested quantifiers give it an exponential number of ways to partition those a characters. Twenty characters can mean roughly a million attempts; thirty can mean a billion. The pattern hangs.

The signature to watch for is nested quantifiers over overlapping alternatives: (a+)+, (a|a)*, (.*)*, (\s|\t)+$. Anything where the inner and outer quantifier can match the same text in multiple arrangements.

Practical defences:

  • Anchor and be specific. Replace .* with a negated class wherever a delimiter exists.
  • Avoid nesting quantifiers over expressions that can match the same input more than one way.
  • Never build a pattern from untrusted input. A user supplied regex is arbitrary CPU consumption on your server, which makes it equivalent to accepting arbitrary code in that one respect.
  • Bound the input length before matching.
  • Use a linear time engine where available, such as RE2 or Rust's regex crate, which reject backreferences and lookaround in exchange for guaranteed linear performance.

Testing a suspicious pattern is easy: run it against a string of 25 to 30 repeated characters that almost matches but fails at the end. If the Regex Tester returns instantly, the pattern is fine; if the tab stalls, you have found the problem before production did.

Quick reference

  • .* grabs everything, then shrinks. Right for "to the end of the line".
  • .*? grabs nothing, then grows. Right for "up to the first delimiter".
  • [^x]* never crosses x at all. Usually the right answer, and the fastest.
  • Multiline input: . does not match newlines unless the s (dotall) flag is set, which is why a greedy .* stays on one line by default. That flag surprises people in both directions.
  • Anchors are free and prevent whole classes of over matching.

The remaining syntax, character classes, groups, anchors, and flags, is collected in the regex cheat sheet, and lookahead and lookbehind covers the assertions that let you match by context without consuming it.