DEV Community

AI Dev Hub
AI Dev Hub

Posted on

Debugging a 41-second regex hang with a regex tester in 2026

Debugging a 41-second regex hang with a regex tester in 2026

Use a regex tester that shows capture groups live and warns about catastrophic backtracking, because the pattern that takes production down is rarely a wrong match. It's usually a pattern that works fine on 20 characters and hangs forever on 40. Paste the expression, paste a real input line, read the group table, then check the backtracking warning before you ship anything.

Full disclosure: the regex tester I link to below is one I built. I'd been bouncing between four online testers for years and every one of them either skipped Go's RE2 flavor or only told me about catastrophic backtracking after the engine had already timed out. Mine is free, runs entirely in the browser, needs no account, and never sends your pattern to a server. If you've got a better one, tell me and I'll switch.

The 41 seconds that broke our log parser

Late April 2026, a Tuesday afternoon. We had a Node service chewing through nginx access logs and turning them into structured events. It had been running in staging for six weeks without a complaint. Then the pod started getting OOM-killed on a loop, and the container logs went completely quiet right before each restart.

The culprit was one line of code. Line 238, a field validator that looked harmless:

^(\s*\w+\s*)+$

That pattern is fine. It matches. It also happens to be exponential, because \s* and \w+ can both consume the same characters, and the outer + lets the engine try every possible split. Feed it a string that almost matches (say, a request path with a trailing character that isn't a word character) and the backtracking explodes.

One malformed log line. 1,247 requests already queued behind it. 41 seconds pinned at 100% CPU on a single call to .test(), and then the kernel took the process out.

Here's the part that annoyed me for most of a day: I debugged it wrong. I did what everyone does. Console.log, a Node REPL, some print statements around the call site. That took 47 minutes and told me absolutely nothing, because a REPL only answers "does this match", and the answer was "yes, eventually." The question I actually needed answered was "how many steps does the engine take to get there, and how does that scale with input length."

A REPL will not tell you that. It just sits there.

What a regex tester actually does under the hood

A good tester isn't a wrapper around String.prototype.match. Three things happen when you paste a pattern in.

First, the pattern gets compiled against the flavor you picked. This matters more than people expect. JavaScript, Python's re, and Go's regexp disagree on lookbehind support, on named group syntax ((?<name>...) vs (?P<name>...)), and on whether backtracking exists at all. Go uses RE2, which has no backtracking, so the pattern that killed our Node service would have run in linear time there. Same regex, wildly different runtime characteristics.

Second, matching runs in a loop with lastIndex tracked manually, so every match and every capture group gets collected instead of just the first. That's what fills the group table. You see group 1, group 2, the named ones, and the exact character offsets, updated as you type.

Third, and this is the one that would have saved me a day: static analysis for nested quantifiers over overlapping character classes. That's the ReDoS signature. (a+)+, (\s*\w+\s*)+, (\w|\d)*$ and friends. The check is a heuristic, so it produces false positives on patterns that are technically safe, but I'd rather see a yellow warning I can dismiss than find out from a pager.

You can reproduce the actual blowup yourself. Save this and run it:

// backtrack.mjs - run with: node backtrack.mjs
const pattern = /^(\s*\w+\s*)+$/;

for (let n = 14; n <= 22; n += 2) {
  const line = "a ".repeat(n) + "!";     // "!" guarantees a failed match
  const t0 = process.hrtime.bigint();
  pattern.test(line);
  const ms = Number(process.hrtime.bigint() - t0) / 1e6;
  console.log(`${n} words: ${ms.toFixed(2)} ms`);
}

// 14 words:    1.02 ms
// 16 words:    3.91 ms
// 18 words:   15.62 ms
// 20 words:   62.45 ms
// 22 words:  249.80 ms
Enter fullscreen mode Exit fullscreen mode

Roughly 4x per two extra words. Extrapolate to a 60-character path and you get my 41 seconds. Nothing in that output requires a debugger or a profiler, but you do have to know to go looking for it, and you won't go looking if your tool only says "no match."

Regex101, RegExr, or a five-line node script

I used regex101 for years and still open it for PCRE work. It has the best debugger of anything in this space, full stop. But the workflow I wanted was different: paste, see the risk immediately, no account, no network round trip, and Go's flavor in the same dropdown as JavaScript. That's the gap I built the aidevhub regex tester to fill, and it's where I'd start if you're chasing a pattern that behaves badly rather than one that's simply wrong.

Checked against the others in July 2026:

What I checked aidevhub Regex Tester regex101 RegExr node -e one-liner
Flavors JavaScript, Python, Go PCRE2, JS, Python, Go, Java, .NET JS, PCRE JS only
Backtracking warning Static check, before you run it Reports it after the engine times out None None
Plain-English breakdown Yes, per token Yes, token list plus debugger Yes, on hover No
Capture group table Yes, live Yes Partial Manual console.log
Account needed to save No, nothing leaves the tab Yes, to save permalinks Yes, to save patterns N/A
Step-by-step debugger No Yes, best in class No No

That "No" in the debugger row is real and I'm not going to hide it. If you need to walk the engine's backtracking path step by step to understand why a pattern fails, regex101 does that and mine doesn't. Different jobs.

When a browser regex tester is the wrong tool

I'd skip all of this in a few cases, and I say that as the person who built one of them.

If your pattern is longer than about 200 characters, the honest answer is that you have a parser, and you should write a parser. I've watched a team maintain a 600-character email validation regex for two years. Every tester in the table above will happily render it, and none of them will make it a good idea.

If you're working against a flavor nobody supports well (Oracle's REGEXP_LIKE, or the subset that ships in some embedded Lua runtimes), test in the actual engine. A JS-flavored tester giving you a green checkmark on a pattern that Oracle parses differently is worse than no tool, because now you're confident and wrong.

If the input is sensitive, read the tool's docs before pasting. Mine runs client-side and I'll say so plainly, but "client-side" is a claim you should verify in the network tab rather than take on faith from a blog post. Open devtools, type a pattern, watch for requests. Thirty seconds.

And if you're already deep in a debugging session with a profiler attached, don't context-switch to a browser tab. The five-line script above lives in your repo and answers the scaling question directly.

One thing I got wrong for a long time: I assumed ReDoS was an exotic security-research problem, something that mattered for input parsers exposed to hostile users. It isn't. Our log line wasn't hostile. It was a truncated request path from a client that hung up mid-request, and it hit a validator I'd written myself and never once tested with input longer than a sample string.

FAQ

Q: Does a ReDoS warning mean my pattern is definitely vulnerable?

A: No. The detection is static and errs toward false positives, so nested quantifiers get flagged even when the surrounding anchors make the bad path unreachable. Treat it as a prompt to run the timing loop above, not a verdict.

Q: Why do JavaScript, Python, and Go produce different results for the same pattern?

A: Different engines. Go's regexp package uses RE2, which guarantees linear time and drops backreferences and lookaheads entirely. JS and Python both backtrack. A pattern that's a landmine in Node is safe in Go and won't even compile if it uses lookbehind.

Q: Can I test replacement strings too?

A: Yes, including $1-style group references in JS mode and \1 in Python mode. This is where the flavor selector earns its place, since getting the replacement syntax backwards is the most common silent bug I hit.

Q: What's the fastest way to fix a catastrophic pattern once I've found one?

A: Usually by making the inner quantifier possessive or atomic where the flavor supports it, or by rewriting so the alternatives can't match the same characters. For our validator, ^\w+(\s+\w+)*$ fixed it. Same matches, linear time.

Written with AI assistance and human review. Try the tool at aidevhub.io/regex-tester.

Top comments (0)