Some regex patterns look harmless and behave perfectly on every input you try, until one particular string makes them hang. The page stops responding, the tab spins, and the only fix is to close it. This is catastrophic backtracking: the regex engine tries an enormous number of ways to match, and the number of attempts grows far faster than the length of the input. Understanding why it happens takes one example, and fixing it usually takes a single rewrite. Regex Tester protects against this with a one-second timeout, so a dangerous pattern fails fast in the tester instead of freezing your browser.

The classic dangerous shape is nested quantifiers: a repeated group that itself contains a repeated element. A well-known example is a pattern meant to match a run of letters followed by an optional ending, written as ^(a+)+$. On the input "aaaa", it matches instantly. On a longer string of letters followed by a character that does not fit, such as thirty letters and then a "b", the engine must try every way of splitting those letters between the inner and outer repetitions before it can give up. The number of splits grows exponentially with the input length. Adding a single character to the input can roughly double the work, which is why a pattern feels fast in testing and freezes in production: the test inputs were just short enough.

Worked example: a pattern that looks innocent

Imagine a fictional form that accepts a simple code made of letters, with an optional trailing section. A developer writes a pattern to match it, using nested repetition because it reads naturally. Test it in the tester against a short fictional input first:

Pattern: ^(a+)+$
Sample: aaaa

That matches at once. Now try a longer sample that should fail, because it ends with the wrong character:

Sample: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaab

The tester hits its one-second timeout instead of returning. That timeout is the safety net doing its job: in a page without one, this same pattern would keep the tab busy far longer, and on a server it could block other work entirely. The important observation is that the failing input is not strange. It is thirty ordinary letters and one wrong character — exactly the kind of input a real user, or a mistyped paste, produces.

The fix is to remove the ambiguity. The pattern ^(a+)+$ has many ways to divide the same letters; the pattern ^a+$ has exactly one. Both match the same successful inputs, but the second cannot backtrack catastrophically because there is nothing to redistribute. Rewrite the pattern that way and the long failing input is rejected immediately. The general rule is simple: never put a quantifier around a group that already contains a quantifier over the same characters, unless you have a specific reason and have tested the failure case.

Other shapes that cause the same problem

Nested quantifiers are the most famous cause, but overlapping alternatives cause the same explosion. A pattern like (a|aa)+$ gives the engine two ways to consume each letter, and on a failing input it must explore combinations of those choices. The fix is the same kind of simplification: choose one way to match each piece of text, and remove the alternative that adds nothing. Similarly, a pattern with two adjacent greedy quantifiers over overlapping character sets, such as .*.*b, forces the engine to try many splits of the same text. Anchoring the pattern, or replacing a greedy .* with a negated class like [^b]*, removes the ambiguity.

Another habit worth breaking is testing only successful inputs. A pattern that matches quickly tells you nothing about how it fails, and failure is where backtracking lives. Always test a long input that should not match, especially one that nearly matches: a long run of the expected characters followed by one wrong character at the end. That near-miss input is the worst case for backtracking, and it is the test that reveals a dangerous pattern before your users do. Test with realistic lengths too, not just short samples. A pattern safe at ten characters can be dangerous at thirty.

Rewriting without losing what the pattern was for

Rewrites should preserve the accepted language — the set of strings the pattern matches — while removing ambiguity. Three techniques cover most cases. First, flatten nested quantifiers, as above: (a+)+ becomes a+. Second, use atomic-style thinking even where the syntax differs: once a part of the match is settled, do not let the engine revisit it. In JavaScript, possessive quantifiers are not available, but restructuring the pattern to remove overlap achieves the same effect. Third, replace .* with a precise character class wherever the matched content has a known shape. A pattern for a quoted string, for example, is safer as "[^"]*" than as ".*" with a lazy quantifier, because the negated class cannot overshoot or redistribute.

After rewriting, re-run the full test set: the successful inputs, the short failures, and the long near-miss. The tester shows matches and groups for each, and the timeout stays as a backstop. If a rewritten pattern still times out, the ambiguity is still there, and the next step is to simplify further rather than to raise the timeout. A timeout is a symptom, not a setting to tune away. For a fuller treatment of safe pattern design, including the timeout itself, see the regex safety guide.

Honest limitations

A one-second timeout catches a hanging pattern, but it cannot tell you whether a pattern is correct, only that it finished in time. A pattern can be fast and still wrong, matching things it should reject or rejecting things it should accept. The tester also runs JavaScript regex, so a pattern proven safe here should still be checked in the language where it will actually run, since engines differ in optimisations and features. Finally, some genuinely complex matching tasks cannot be expressed safely as a single regex; in those cases, splitting the check into simpler steps in code is better than crafting a clever pattern.

FAQs

What is catastrophic backtracking?

It is a situation where a regex engine tries an exponentially growing number of ways to match an input, usually because nested quantifiers or overlapping alternatives give it many ways to divide the same text. On a failing input, the engine must exhaust those combinations before giving up.

How do I know if my pattern is dangerous?

Test it against a long input that should fail, especially a long run of expected characters ending in one wrong character. If the tester times out or the match takes noticeably long, the pattern has a backtracking problem.

What does the one-second timeout do?

It stops a pattern that runs too long and reports the timeout, instead of letting it freeze the browser tab. The timeout is a safety net for testing; a pattern that hits it needs rewriting, not a longer allowance.

How do I fix a backtracking pattern?

Remove the ambiguity: flatten nested quantifiers, drop overlapping alternatives, and replace broad .* matches with precise character classes like [^"]*. Then re-test with the long near-miss input that exposed the problem.

Patterns that process document text often sit alongside other tools: JSON Studio validates structured input the pattern may run against, and Text Compare shows what a pattern-based replacement changed.

human review pending client designation