← All Tools

Greedy vs Lazy Regex Matching and Catastrophic Backtracking (ReDoS) Explained

Guide · Last verified Aug 26, 2026

Work with quantifiers like + and * in regex long enough and eventually you'll hit a moment where a browser tab or server process just freezes solid. When that happens in a regex validating user input, it's not just a bug — it becomes an open door for denial-of-service (DoS) attacks. This guide breaks down the greedy and lazy matching behavior behind it, and walks through real examples of why nested quantifiers cause exponentially slow "catastrophic backtracking."

1. Greedy matching — the default behavior of quantifiers

Quantifiers like + (one or more), * (zero or more), and {n,} (n or more) are greedy by default in regex engines. Greedy means the engine first consumes as many characters as it possibly can. For example, apply <.+> to the string <b>bold</b>, and .+ first swallows everything to the end of the string, then gives characters back one at a time until it finds a position where the following > matches (that giving-back process is backtracking). The result is that the whole thing matches from the first <b> all the way through the last </b>, rather than just the first tag. Quantifiers with a question mark appended — +?, *? — are lazy instead: they try the minimum length (0 or 1 characters) first, and only consume one more character when the rest of the pattern fails. Apply <.+?> to the same string and it stops right at the first <b>, matching only that one tag. So the difference between greedy and lazy is really just direction — "eat a lot first, then back off" versus "eat little, then push forward" — both rely on the same underlying mechanism: backtracking.

2. Why nested quantifiers are dangerous — an exponential explosion of paths

The real trouble isn't a single quantifier, but a quantifier nested inside another quantifier — patterns like (a+)+ or (a|aa)+. These patterns can end up producing the same overall match result by splitting the same stretch of the string into groups in many different ways. Matching (a+)+ against aaaa, the inner group could swallow all of aaaa in one shot, or split it as aaa + a across two passes, or aa + aa, or a + a + a + a — every possible way of chopping the characters into groups is equally valid. The number of ways to split n a's grows roughly proportional to 2^(n-1). If the input ultimately matches, the engine stops as soon as it finds one valid combination, so there's no real problem. But attach something that can never match — a single ! at the end of the string, for instance — and everything changes. To confirm that not a single combination succeeds, the engine has to try every possible way of splitting the string via backtracking, and that's exactly what "catastrophic backtracking" is.

Real example: try the regex /(a+)+$/ against a string made of 50 a's in a row followed by an exclamation mark that guarantees the match can never succeed — that is,
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!" — and you'll see "no match" pop up almost instantly at short lengths (10-15 a's), but as the count climbs to 30, 40, then 50, the processing time noticeably doubles each time you add a few more. $ only matches at the very end of the string, and the trailing ! makes success impossible from the start, so the engine has to try every way of splitting up a+ before it can even confirm failure. At 50 a's, that's roughly 2^49 paths to check — on a real server, a single request like this can pin one CPU core essentially forever.

3. How to avoid this in practice

The most reliable fix is to simply never wrap the same character set in two layers of quantifiers. (a+)+ ultimately matches the exact same language as a+, so stripping the outer group and quantifier solves it. You should be equally suspicious of patterns like (a|aa)+, where the alternatives inside the group overlap. Second, use anchors like ^ and $ appropriately to limit how far backtracking can spread in the first place, and favor specific character classes like [0-9] over broad ones like . where possible. Third, since JavaScript's native RegExp doesn't support backtracking-suppression syntax like atomic groups or possessive quantifiers, if a situation lets user input determine the regex itself (search filters, custom validation rules, etc.), placing a hard limit on execution time is the fundamental defense. In Node.js, common approaches include running regex execution in a worker thread with a forced timeout, or swapping in a linear-time engine from the RE2 family. Ultimately, this isn't a minor syntax slip — it's an algorithmic limitation of backtracking-based NFA engines — so you need both "write patterns carefully" and "make sure a dangerous pattern can't take the whole process down" working together.

4. Summary — a checklist for writing regex

Frequently Asked Questions

Q. What exactly is the difference between greedy and lazy matching?

A. Quantifiers like +, *, and {n,} default to greedy matching: they first consume the maximum possible length, then give characters back one at a time (backtracking) whenever the rest of the pattern fails, until the whole thing matches. Adding a question mark — +?, *? — makes them lazy instead: they start by trying the minimum length and only consume one more character at a time when the rest of the pattern fails.

Q. Why is a pattern like (a+)+ dangerous?

A. Because the outer + and the inner + can split the same characters into groups in many different ways. For example, splitting six a's into 6 groups or into 3 groups both end up matching the whole string 'aaaaaa,' so the number of such combinations explodes to roughly 2^n as the character count grows. When the input ultimately fails to match, the engine has to try every one of those combinations via backtracking, which makes it look like it has simply frozen.

Q. What real damage can ReDoS do to a live service?

A. If server code validating user input (email, password, URL, etc.) contains a vulnerable regex, an attacker only needs to send a single carefully crafted long string designed to ultimately fail to match, and the thread or process handling that request pegs its CPU at 100% and hangs. Even a handful of concurrent requests like that can push the whole server into a denial-of-service state where it can't process anyone else's requests.

Q. Is it safe to test dangerous patterns with this site's regex tester?

A. regex-tester.html runs whatever pattern you enter directly in the browser's JavaScript engine, with no execution time limit of its own. Combining a catastrophic-backtracking pattern with a long input can make that tab become unresponsive, so when experimenting with dangerous patterns, start with short strings (around 10-20 characters) and increase the length gradually to feel out the effect.

Q. How do I fix a nested quantifier to make it safe?

A. The key is not wrapping the same character set in two layers of quantifiers. Check first whether you can collapse (a+)+ into a+, or (a|aa)+ into a+, by removing the outer quantifier and consolidating into a single inner one. If the group structure is genuinely necessary, design the alternatives inside it to be mutually exclusive so they don't match overlapping strings, and if you're still not confident, it's safer to use a regex library that enforces an execution time limit on the engine itself, such as re2 or safe-regex.