Catastrophic backtracking: why a regex can pin a CPU

Nested quantifiers make the number of match attempts grow exponentially; (a+)+b can hang a process on one long non-matching input. Drop the nesting or put a timeout on it.

When a single regex freezes a service, it is usually catastrophic backtracking: the number of combinations the engine tries grows exponentially with input length.

The shape that breaks

/^(a+)+b$/.test('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')

There is no b, so it can never match. But before giving up, the engine tries every way to split those a characters: the outer + and inner + can divide them freely, giving 2 to the n combinations. Thirty a characters is a billion attempts.

The warning sign is a quantifier inside a quantifier: (x+)*, (x*)*, or (x|y)+ where both branches match the same input.

Fix one: remove the nesting

That regex actually means “all a, then one b”:

/^a+b$/

A quantifier with no nesting is linear. Most catastrophic regexes are fine once hand-rewritten without nesting.

Fix two: make the group atomic

When you genuinely need a capture group, make the inner group atomic or possessive so the engine cannot backtrack into it:

/^(?:a++)+b$/     // possessive quantifier

JavaScript has lookbehind since ES2018, but atomic groups only arrive in ES2025, so rewriting is the usual route today.

Fix three: bound the input

If the domain never exceeds 200 characters, reject longer with an early return. Exponential growth stops being scary when the base is bounded.

Fix four: add a timeout

The sturdiest fallback is trusting no regex at all:

function safeTest(re, s, tag) {
  const start = Date.now();
  const worker = new Worker(/* run re.test(s) on its own thread */);
  // on timeout, terminate and return false
}

Node has no built-in regex timeout, so isolation into a thread or process is the only option. Engines that do not backtrack, such as RE2, are immune by construction, at the cost of backreferences and lookbehind.

Checklist

What you see Risk
(a+)* (a*)* high, exponential
`(a a)*` overlapping branches
User-supplied regex high, equals remote code
Fixed literal regex low, no backtracking

The last row is the most overlooked: never let users submit a regular expression. It is not a config value; it is a Turing-complete program that runs on your server.

A quantifier inside a quantifier is the alarm bell. If you cannot rewrite it flat, add a timeout.

← Back to all posts

Comments

…