Six characters that freeze a regex tester
Type (a+)+b into a regex tester, then hold down the A key. Somewhere around forty characters the page stops answering: no highlighting, no typing, no scrolling, and the tab has to be closed. The pattern is six characters long, and somebody writing a validator for a comma separated field arrives at it by accident.
The numbers, so the shape is visible
/^(a+)+b$/ tested against a string of A's ending in a C, which cannot match. Node 24.15.0, one machine, 2026-09-11.
| Characters | Time | Against the row above |
|---|---|---|
| 19 | 5.26 ms | |
| 20 | 11.95 ms | 2.3x |
| 22 | 48.57 ms | 2.1x |
| 24 | 191.29 ms | 1.7x |
| 26 | 1,428.73 ms | 3.4x |
| 28 | 4,770.71 ms | 2.1x |
Every character roughly doubles the work, because (a+)+ can split a run of A's into groups in exponentially many ways and the engine tries all of them before admitting there is no B. Extending the measured curve, 40 characters is about nine hours and 50 is over a year. That is an extrapolation and not a measurement, for the obvious reason.
Where the time is spent
Catastrophic backtracking is well documented. The consequence that follows from where the time is spent is documented less, and it decides how a tester has to be built.
All of it is inside a single call to RegExp.prototype.test. JavaScript is one thread and that call does not yield. So this, which is the obvious defence and appears in a lot of code, does nothing at all:
// Does not work. The timer cannot fire until test() returns, // and test() is what you are trying to give up on. const stop = setTimeout(() => giveUp(), 2000); const matched = pattern.test(subject); clearTimeout(stop);
The callback is queued behind the very call it is meant to interrupt. Same for a deadline checked in a loop, since there is no loop of yours to check it in, and same for AbortController, which regular expressions do not accept. There is no API that stops a running regular expression. That is not an oversight; the engine has no safe point to be stopped at.
So the only lever is the one that kills the whole thread
Worker.terminate() stops a worker immediately, mid-instruction, without asking it anything. It is a blunt instrument and it is the only one available. Which settles the architecture: the match cannot run on the page, because a page cannot be terminated. It runs in a worker, and the page holds a timer that throws the worker away.
const runner = new Worker(url);
const killer = setTimeout(() => {
runner.terminate(); // the only thing that stops it
show("that pattern did not finish");
}, 2000);
runner.onmessage = ({ data }) => {
clearTimeout(killer);
show(data.matches);
};Measured on our own page, 2026-09-10, with (a+)+b typed against 41 characters: a 3,000ms timer on the main thread came back in 3,002ms. The page kept painting and typing throughout, and the warning appeared at two seconds. The pattern that would have ended the tab instead produced a sentence about nested repetition.
Three things that came out of building it this way
The worker is a blob, not a file. The worker source is a string in the bundle, turned into a Blob and an object URL at the moment it is needed. No second request, so the tool still works with the network off, which is the promise every page here makes.
The replace preview runs on the matches, not the pattern. Re-running the pattern to build a preview would put a second execution on the main thread, which is the thing this whole design exists to avoid. Expanding $1 and $<name> against the match list that already came back costs nothing and cannot disagree with the highlighting above it.
The error messages needed rewriting. V8 answers (?<>x) with "Invalid capture group name", which names the fault and not the fix. Ours says a named group is written (?<name>...). Anything we have not seen is passed through untouched, because a strange message that names the real problem beats a friendly one that does not.
How to check your own tester
Paste (a+)+b as the pattern and about forty A's as the subject. A tester that runs on the page will stop responding within a second or two and stay that way. One that runs in a worker will tell you it gave up. It is a two second test and it tells you whether anyone thought about it.
The tool this came out of
Live matching, capture groups, a replace preview and a two second budget, all in your browser with nothing uploaded.
filetity is built by Adarsh Mishra. The timings above came from one machine on one evening and the script that produced them is four lines, so they are worth re-running rather than trusting. If your numbers disagree, that is worth knowing: support@filetity.com.