CodeKitHub
English
Regex greedy vs lazy matching: why your pattern grabs too much

Regex greedy vs lazy matching: why your pattern grabs too much

Published Jul 24, 2026

You write <b>.*</b> to grab the contents of a bold tag, test it on <b>Hello</b>, it works perfectly. Then you run it on real HTML with two bold tags on the same line — <b>Hello</b> and <b>World</b> — and instead of two matches, you get one giant match spanning both tags. This isn’t a bug in your regex engine; it’s the default behavior of *, +, and {n,m}, and it has a one-character fix once you know what’s happening.

Why .* grabs more than you expect

By default, quantifiers in regex are greedy.* doesn’t mean “match some characters,” it means “match as many characters as possible, then back off only if the rest of the pattern absolutely requires it.”

Walk through <b>.*</b> against <b>Hello</b> and <b>World</b> step by step:

  1. <b> matches the first <b>.
  2. .* starts by consuming the entire rest of the string — everything up to the end, including the second <b>World</b>.
  3. The engine then needs to find </b> to finish the match, so it starts backing off from the end of .* one character at a time.
  4. The first place (scanning backward from the end) where </b> fits is the very last </b> in the string — not the first one it “logically” should stop at.

So the match ends up being <b>Hello</b> and <b>World</b> in its entirety, because greedy matching always tries the longest possible string first and only shrinks it as little as necessary to make the rest of the pattern succeed.

The fix: make the quantifier lazy with ?

Adding a ? immediately after a quantifier flips it from greedy to lazy (also called “non-greedy” or “reluctant”): *?, +?, ??, {n,m}?.

A lazy quantifier does the opposite: it starts by matching as few characters as possible, then only expands if the rest of the pattern can’t succeed yet.

<b>.*?</b> against the same string:

  1. <b> matches the first <b>.
  2. .*? starts by matching zero characters.
  3. The engine checks: does </b> match right here? No (we’re at “Hello…”, not yet at a </b>) — so it expands .*? by exactly one character and checks again.
  4. This repeats character-by-character until .*? has consumed exactly Hello, at which point </b> matches immediately.

Result: <b>Hello</b> and <b>World</b> come back as two separate matches, which is almost always what you actually wanted when parsing tag-like or delimiter-like structures.

When you actually want greedy (it’s not just “the wrong default”)

Greedy isn’t a mistake in the language — it’s correct for a different, equally common case: matching the outermost boundary of something, not the smallest unit inside it. If you’re extracting “everything between the first { and the last }” in a blob of JSON-like text (say, to grab a whole object regardless of nesting), greedy is exactly right and lazy would stop at the first inner } instead, giving you a truncated, invalid fragment.

The rule of thumb: lazy for repeated small delimited chunks (tags, quoted strings, list items); greedy for “grab the whole outer span.”

Quick reference

Pattern Behavior Use it when
.*, .+, {n,m} Greedy — matches longest possible You want the outermost span, or there’s only one match in the string
.*?, .+?, {n,m}? Lazy — matches shortest possible You have multiple similar delimited chunks and want each one separately

If you’re not sure which one a pattern is actually doing on your real input rather than your test string, running it against the actual multi-match text in a regex tester with live match highlighting makes the difference immediately visible — you’ll see one giant highlighted block for greedy versus several separate ones for lazy, which is usually faster than reasoning through it character-by-character.

← Back to Blog