Developer Tools

What is the difference between greedy and lazy quantifiers?

Understand greedy vs lazy quantifiers in regex, how they affect matching behavior, and when to use each approach.

By Inventive HQ Team

Greedy vs Lazy Quantifiers in Regex

A greedy quantifier (*, +, ?, {n,m}) grabs as much text as possible and then backtracks until the rest of the pattern matches; a lazy quantifier — the same symbol with a trailing ? (*?, +?, {n,m}?) — grabs as little as possible and then expands until the pattern matches. They can finish on the exact same match; the only difference is which end of the range the engine tries first. That single choice decides whether <.*> captures one HTML tag or the entire line.

That is the summary an AI Overview will give you. What it can't show you is the step-by-step engine trace that makes the behavior click, the moment a greedy match is actually the right call, or the trap where switching to lazy hides a catastrophic-backtracking bug instead of fixing it. Below is an animated trace of both modes running on the same input, a side-by-side comparison table, and a decision matrix that tells you exactly which quantifier — greedy, lazy, or a negated character class — to reach for.

Greedy versus lazy matching on the text <a><b> A greedy quantifier expands to the end of the string then backtracks to the last delimiter, while a lazy quantifier starts minimal and expands to the first delimiter. Pattern <.*> on text: <a><b> Greedy <.*> Expand to the end, then backtrack < a > < b > 1. .* eats a><b> (max) 2. no > left → back up 3. final > matches at end match: <a><b> whole line — usually not what you want Lazy <.*?> Start minimal, expand to first stop < a > < b > 1. .*? matches nothing 2. need > → expand by a 3. next > matches → stop match: <a> one tag — with /g, then <b> next
Same pattern, same text, opposite starting strategies. Greedy overshoots then walks back; lazy creeps forward and stops early.

By default, regex quantifiers are greedy—they match as much text as possible while still allowing the overall pattern to match. This behavior is often correct, but sometimes you need the opposite: to match as little text as possible. Understanding when to use each approach and how to switch between them is essential for writing correct regex patterns.

What Makes a Quantifier "Greedy"?

Greedy quantifiers match the maximum possible amount of text:

Greedy Quantifiers:

*   - Zero or more (greedy)
+   - One or more (greedy)
?   - Zero or one (greedy)
{n,m} - Between n and m (greedy)

When a regex engine encounters a greedy quantifier, it first tries to match as much as possible. Only if the rest of the pattern fails to match does it backtrack (reduce the match) and try again.

Example: The Problem with Greedy

Text: <div>Hello</div><span>World</span>
Pattern: <.*>

You might expect this to match <div> or <span>, but instead:

Match Result: <div>Hello</div><span>World</span>
Why: .* greedily matches EVERYTHING between < and >
     It matches from the first < through the last >

The .* matches div>Hello</div><span>World</span> leaving only the final > for the pattern to complete.

What Makes a Quantifier "Lazy"?

Lazy (non-greedy) quantifiers match the minimum possible amount of text. Make any quantifier lazy by adding ? after it:

Lazy Quantifiers:

*?  - Zero or more (lazy)
+?  - One or more (lazy)
??  - Zero or one (lazy)
{n,m}? - Between n and m (lazy)

When a regex engine encounters a lazy quantifier, it first tries to match as little as possible. Only if the overall pattern fails does it expand and try again.

Example: The Solution with Lazy

Text: <div>Hello</div><span>World</span>
Pattern: <.*?>

Now the pattern matches:

Match 1: <div>
Match 2: </div>
Match 3: <span>
Match 4: </span>
Why: .*? matches minimally, stopping at the first > after <

This is typically what you want when matching HTML/XML tags.

Greedy vs Lazy vs Possessive: The Full Comparison

Most explanations stop at "greedy vs lazy," but production regex engines actually give you three modes. Here is how they line up side by side:

PropertyGreedy .*Lazy .*?Possessive .*+
Tries firstMaximum matchMinimum matchMaximum match
On failureBacktracks (gives characters back)Expands (takes more characters)Never backtracks — fails immediately
Default?YesNo (add ?)No (add +)
Typical useMatch to end of line, whole contentContent between delimitersLocking a match to stop catastrophic backtracking
Backtracking costCan be highModerateZero
JS / Python re supportYesYesNo (use atomic groups / no equivalent in JS)
PCRE / Java / Ruby / .NETYesYesYes

Note the last row: possessive quantifiers and atomic groups exist in PCRE, Java, Ruby, and .NET, but JavaScript and Python's built-in re module have neither — in those languages you avoid catastrophic backtracking by restructuring the pattern (for example, a negated character class) rather than by making it possessive.

Advertisement

Decision Matrix: Which Quantifier Do I Reach For?

If your goal is…Best choicePattern shapeWhy not the alternatives
Grab everything to the end of the lineGreedy.*$Lazy would stop too early
Extract text between a single-char delimiter (>, ", ))Negated class<[^>]*>Faster than .*?, can't overshoot, no backtracking
Extract text between a multi-char delimiter (</div>, -->)Lazy<!--(.*?)-->A class can't express a multi-char boundary
Match repeated separate items with the global flagLazy".*?" with /gGreedy merges them into one giant match
Consume a fixed token you know can't nestPossessive / atomic"[^"]*+" (PCRE)Prevents exponential blow-up on malformed input
You genuinely aren't sureLazy first.*?More intuitive; then tighten to a negated class once it works

The single most useful takeaway: when you're tempted to write .*? to stop at a specific character, check whether a negated character class does the job instead. <[^>]*> is greedy, needs no backtracking, and cannot accidentally cross a > — it beats <.*?> on both correctness and speed.

Detailed Comparison

How Greedy Matching Works

When a regex engine encounters a greedy quantifier:

  1. Expand: Match as much as possible
  2. Check: Does the rest of the pattern match?
  3. Result: If yes, done! If no, backtrack and try again
  4. Backtrack: Reduce match by one character
  5. Repeat: Go back to step 2

Example with Trace:

Text: "aaab"
Pattern: a+b

Step 1: a+ matches "aaa" (greedy, maximum)
Step 2: Try to match "b" at position after "aaa"
Step 3: "b" matches!
Result: "aaab" ✓

How Lazy Matching Works

When a regex engine encounters a lazy quantifier:

  1. Minimize: Match as little as possible
  2. Check: Does the rest of the pattern match?
  3. Result: If yes, done! If no, expand and try again
  4. Expand: Match one more character
  5. Repeat: Go back to step 2

Example with Trace:

Text: "aaab"
Pattern: a+?b

Step 1: a+? matches "a" (lazy, minimum)
Step 2: Try to match "b" at next position
Step 3: "a" doesn't match "b", expand
Step 4: a+? matches "aa"
Step 5: Try to match "b" at next position
Step 6: "a" doesn't match "b", expand
Step 7: a+? matches "aaa"
Step 8: Try to match "b" at next position
Step 9: "b" matches!
Result: "aaab" ✓ (same as greedy here)

When Greedy and Lazy Produce Different Results

Extracting Content from Tags

Text: <div>Hello</div><span>World</span>
Pattern (Greedy): <div>(.*)</div>
Pattern (Lazy): <div>(.*?)</div>

Greedy Match:

Captured: Hello</div><span>World
Result: Everything between <div> and the LAST </div>

Lazy Match:

Captured: Hello
Result: Everything between <div> and the NEXT </div>

The lazy version correctly captures just the content.

Extracting Quoted Strings

Text: "hello" and "world"
Pattern (Greedy): "(.*)"
Pattern (Lazy): "(.*?)"

Greedy Match:

Captured: hello" and "world
Result: Content between first " and last "

Lazy Match:

Match 1: "hello"
Match 2: "world"
Result: Each quoted string separately

Multiple Alternatives

Text: aaaaab
Pattern (Greedy): a*ab
Pattern (Lazy): a*?ab

Both match "aaaaab", but for different reasons. The greedy a* first swallows all five as, fails to find the literal a the pattern still needs, and backtracks one step so ab can match. The lazy a*? starts with zero as and expands until ab finally matches at the end. Same result, opposite journeys.

Practical Examples

Example 1: HTML Tag Extraction

// Greedy - WRONG
let html = "<h1>Title</h1><p>Content</p>";
let match = html.match(/<.*>/);
// Result: "<h1>Title</h1><p>Content</p>" (entire string!)

// Lazy - CORRECT
let match = html.match(/<.*?>/);
// Result: "<h1>" (just first tag)

// Get all tags with lazy quantifier
let tags = html.match(/<.*?>/g);
// Result: ["<h1>", "</h1>", "<p>", "</p>"]

Example 2: Extract Quoted Strings

import re

text = 'She said "hello" and "goodbye"'

# Greedy - WRONG
matches = re.findall(r'"(.*)"', text)
# Result: ['hello" and "goodbye'] (too much)

# Lazy - CORRECT
matches = re.findall(r'"(.*?)"', text)
# Result: ['hello', 'goodbye'] (correct)

Example 3: CSV-like Data

Text: name,age,"Smith, John",30
Pattern (Greedy): "(.*)"
Pattern (Lazy): "(.*?)"

Greedy would match too much (from first quote to last quote) Lazy correctly matches quoted fields individually

Example 4: URL Parameter Extraction

// Greedy - WRONG
let url = "?param=value1&other=value2";
let params = url.match(/=.*/);
// Result: "=value1&other=value2" (too much)

// Lazy - CORRECT
let param = url.match(/=.*?(&|$)/);
// Result: "=value1&" (correct, uses lookahead)

Performance Implications

Greedy Matching:

  • First tries maximum match
  • Backtracks if needed
  • Generally faster when match succeeds quickly

Lazy Matching:

  • First tries minimum match
  • Expands if needed
  • Generally faster when pattern is short

Catastrophic Backtracking Risk:

  • Greedy quantifiers with complex patterns can cause exponential backtracking
  • Lazy quantifiers reduce this risk somewhat
  • Better solution: Be specific with patterns, avoid nested quantifiers

Example of Problematic Greedy Pattern

(a+)+b   // Dangerous! Can cause catastrophic backtracking

If the text is "aaaaaaaaaaaa" (no 'b'), the engine tries exponential combinations.

Better:

(a)+b    // Better
a+b      // Best

Choosing Greedy vs Lazy

Use Greedy When:

  • You want to match as much as possible
  • You're looking for the end of text: .*$
  • You want the entire content: .* for whole-line matching
  • Performance is critical and pattern is simple

Use Lazy When:

  • You need to stop at a specific point
  • Extracting content between delimiters: <.*?>
  • Matching quoted strings: ".*?"
  • Matching multiple separate items: .*? with global flag

Decision Questions:

  1. "Do I want to match UP TO the next occurrence of X?" → Use lazy
  2. "Do I want to match EVERYTHING until the pattern ends?" → Use greedy
  3. "Am I unsure?" → Try lazy first (more intuitive for most people)

Converting Between Greedy and Lazy

From Greedy to Lazy: Add ? after the quantifier

.* → .*?
.+ → .+?
.? → .??
.{3,5} → .{3,5}?

Testing the Difference:

  1. Try your pattern with greedy quantifier
  2. If it matches too much, change to lazy
  3. If it matches too little, change to greedy

Common Mistakes

Forgetting Lazy is an Option

WRONG: <.*> trying to extract tags (matches everything)
RIGHT: <.*?> (matches single tags)

Using Lazy Unnecessarily

Inefficient: a+?b (lazy quantifier adds complexity)
Better: ab (if you want single 'a')

Combining Greedy with Greedy

WRONG: .*.*  (two greedy quantifiers, wasteful)
RIGHT: .* (single greedy quantifier)

Summary Table

TaskPatternTypeWhy
Match everything.*GreedyWant maximum match
Extract tag content<(.*?)>LazyStop at closing tag
Match quoted string"(.*?)"LazyStop at closing quote
Match to end of line.*$GreedyWant everything until end
Match pairsa(.*?)bLazyStop at first b
Match all words\w+GreedyMatch complete words

Conclusion

Greedy and lazy quantifiers are complementary tools in your regex toolkit. By default, quantifiers are greedy—they match as much as possible. When you need to match minimally (stopping at the first opportunity), add ? to make the quantifier lazy. Most text extraction and delimiter-based matching tasks benefit from lazy quantifiers. When in doubt, start with lazy quantifiers for pattern matching between delimiters, and use greedy quantifiers for matching to the end of text or when you specifically want maximum matching. Testing patterns with sample data reveals which approach works correctly for your specific case.

Frequently Asked Questions

What is the difference between greedy and lazy quantifiers?

A greedy quantifier (*, +, ?, {n,m}) matches as much text as possible, then backtracks character-by-character until the rest of the pattern can match. A lazy quantifier — the same symbol followed by ? (*?, +?, ??, {n,m}?) — matches as little as possible, then expands one character at a time until the pattern succeeds. Both can end up matching the same text; the difference is which end of the range they try first.

How do you make a quantifier lazy?

Add a ? immediately after the quantifier. .* becomes .*?, .+ becomes .+?, \\d{2,4} becomes \\d{2,4}?. The trailing ? is not the "zero-or-one" quantifier here — it is a modifier that flips the preceding quantifier from greedy to lazy.

Why does <.*> match the whole line instead of one tag?

Because .* is greedy: it first consumes every character to the end of the line, then backtracks to find the last > that still lets the pattern match. On <a><b> it matches <a><b>, not <a>. Use the lazy form <.*?> (or better, the negated class <[^>]*>) to stop at the first >.

Is a negated character class better than a lazy quantifier?

Usually yes. <[^>]*> is greedy but can never overshoot the delimiter, so it needs no backtracking and runs faster and safer than <.*?>. Reach for a negated class whenever the "stop" character is a single known character; use lazy quantifiers when the boundary is a multi-character sequence a class can't express.

Are lazy quantifiers faster than greedy ones?

Not inherently. Lazy is faster when the match is found early in the text; greedy is faster when the match is near the end. The real performance danger is not greedy-vs-lazy but nested quantifiers like (a+)+ that trigger catastrophic backtracking. Switching such a pattern to lazy does not fix the exponential blow-up — restructuring the pattern does.

Do greedy and lazy quantifiers ever match the same thing?

Yes. On the text aaab, both a+b and a+?b match the full aaab because the trailing b forces the engine to consume all three as either way. They differ only when there is more than one valid stopping point — for example capturing content between repeated delimiters.

Does possessive quantification relate to greedy and lazy?

Yes — possessive quantifiers (*+, ++, {n,m}+) and atomic groups are a third mode: greedy but with backtracking disabled. They match maximally like greedy quantifiers but never give characters back, which prevents catastrophic backtracking. They are supported in PCRE, Java, and Ruby, but not in JavaScript or Python's built-in re module.

Which languages support lazy quantifiers?

Lazy quantifiers are supported in virtually every modern regex engine, including JavaScript, Python, Java, .NET, PCRE, Ruby, Go, and PHP. The ? suffix syntax is consistent across all of them, so a pattern like ".*?" behaves the same way in each.

regexquantifiersgreedylazypattern matching