A bad regex is a production outage, not a typo
It is easy to treat a regular expression as a small, contained thing — a pattern you write once, glance at, and ship. The clearest counterexample on record is a postmortem Cloudflare published about its own network, and it is worth reading in full rather than taking secondhand.
On 2 July 2019, a single rule deployed to Cloudflare’s Web Application Firewall contained a regular expression with the classic catastrophic-backtracking shape — a repeated group whose own contents could also repeat, so the engine tried an exponential number of ways to fail a match before giving up. Because a WAF rule runs against every request passing through, that one pattern drove CPU usage toward 100% across Cloudflare’s global network almost immediately. With core proxy, CDN and WAF capacity pegged, the network served 502 errors to a large share of its traffic for roughly 27 minutes before the rule was pulled.
Two contributing factors turned a bad pattern into a global incident rather than a
contained one. The rule went out everywhere in one step, with no staged or canary rollout
that would have surfaced the CPU spike on a slice of traffic before it reached all of it.
And a CPU-usage safeguard that had once existed specifically to catch a runaway regular
expression before it consumed a whole core had been removed in an earlier, unrelated
refactor. Either mitigation alone — the staged rollout, or the reinstated CPU guard
— would likely have kept the incident local. The regex engine involved matched by
backtracking with no runtime guarantee against this class of input, which is not a defect
unique to that one engine: most regex engines in everyday use, including PCRE, Python’s
re and JavaScript’s own RegExp, share the same backtracking
design and the same exposure.
This is the general class of bug called ReDoS — regular expression denial of service — and OWASP’s page on it is a good next stop. The vulnerability is not a mistake in one pattern’s logic so much as a structural property some patterns have: ordinary test input never exercises it, and adversarial — or simply unlucky — input can trigger it by accident. That is exactly why this page runs every match inside a Web Worker with a hard deadline it can terminate, rather than directly on the page’s main thread; see how that timeout actually works above.
A pattern that looks fine against the three test strings you tried by hand can still be the one that hangs on the fourth. Testing a pattern against realistic — and deliberately adversarial — input before it ships is the cheap step that catches this, and it is what the tester on this page is for.
Read the reporting yourself
- Cloudflare — “Details of the Cloudflare outage on July 2, 2019” The company’s own published postmortem: root cause, timeline, and the fixes that followed.
- OWASP — Regular expression Denial of Service (ReDoS) The general vulnerability class: what makes a pattern susceptible, and how to recognise the shape.
Copy-paste pattern library
Nine ready patterns for the problems that come up again and again. Every one targets JavaScript’s own regex flavour — see flavour honesty below for exactly where that differs from PCRE or Python. Click Try it to load a pattern straight into the tester above with its own worked example, or Copy to take just the pattern.
Flavour honesty: this is JavaScript’s regex, not PCRE or Python’s
Every pattern on this page runs through JavaScript’s built-in
RegExp. That flavour is close to, but not identical with, the PCRE engine
behind PHP and most command-line grep-style tools, or Python’s re
module. Copying a pattern between them without adjustment is a common source of
“this worked everywhere except my browser” bugs. Specifically:
- Named groups. JavaScript spells a named capturing group
(?<name>…). Python spells the same thing(?P<name>…). Pasting Python’s form into a JavaScriptRegExpthrows aSyntaxError— this page will show you that error rather than silently failing. - Lookbehind.
(?<=…)and(?<!…)are supported in current Chrome, Firefox and Edge, and in Safari from version 16.4 (March 2023). A pattern using lookbehind will throw in older Safari and in some embedded JS engines. PCRE and Python have supported lookbehind for much longer, so a pattern working in a Python script may still fail in an old browser. - No possessive quantifiers, no atomic groups. PCRE offers
a++(possessive) and(?>…)(atomic) specifically to cut off backtracking early, which is also a standard defence against catastrophic backtracking. JavaScript has neither. That is one reason a pattern that is safe in PCRE can still be dangerous here — see the ReDoS section below. - No recursion. PCRE supports recursive patterns
(
(?R)) for matching nested structures such as balanced parentheses. JavaScript regex cannot recurse at all; balanced-nesting problems need a real parser, not a pattern. \d,\wand Unicode. Without theuflag, JavaScript’s shorthand classes are ASCII-only, same as PCRE’s default. Withu, JavaScript still does not expand\dto every Unicode decimal digit — for that you need the explicit Unicode property escape\p{Nd}, which requires theu(orv) flag and is not available in every engine that claims Unicode support.
None of this makes JavaScript’s regex worse — it is simply a different
implementation of a specification (ECMA-262) that overlaps heavily with, but is not
identical to, PCRE or Python’s re. The patterns in the library above are
written to work as JavaScript regex specifically, and the tester on this page only ever
compiles and runs JavaScript regex — if you paste a PCRE- or Python-only construct,
you will see the SyntaxError immediately rather than a confusing runtime
difference later.
How it works
Compiling a pattern
Every pattern you type is handed to new RegExp(pattern, flags), wrapped in
a try/catch. Constructing a RegExp from bad syntax
— an unmatched [, a dangling (, a quantifier with nothing
before it — throws a SyntaxError, and without the wrapper that
exception would be unhandled and the page would break. Here it becomes the message you see
under the pattern field instead: the engine’s own words for what is wrong, immediately,
with no separate “validate” step required.
Stopping a runaway pattern from freezing the tab
A hand-written regex tester has one failure mode that matters more than any other:
catastrophic backtracking. The textbook example is
/(a+)+$/ against a string of many as followed by a character that
can never match, such as aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!. The inner
+ and the outer + both repeat, so the engine tries an exponential
number of ways to divide the same run of as between them before it can
conclude there is no match. On a string of thirty characters that is already billions of
attempts; add a few more characters and it stops being something you wait out.
This page defends against that in three layers, and it is worth being precise about what each one actually does, because two of the obvious-sounding defences do not really work:
- A heuristic warning, before running anything. The pattern text is
scanned for the textbook shape — a group that can itself repeat, wrapped in another
repetition, like
(a+)+or([a-z]*)*. If it matches, you see a warning badge before the tester runs the pattern at all. This is advisory only: it will miss some genuinely catastrophic patterns (the danger can hide behind alternation or backreferences a simple text scan cannot see) and could in principle flag a rare safe one, so it is a hint, never a block. - A capped iteration count — but this is not the real fix. The match loop stops after 20,000 iterations and reports that it was truncated. It is tempting to assume this is what prevents a freeze, but it is not: catastrophic backtracking happens inside a single call to the engine’s match function, before that call ever returns to the loop that is counting iterations. Capping the number of loop iterations only helps a different, unrelated problem — a pattern that matches validly but produces an enormous or endless number of matches (for instance, a pattern that can match a zero-length string, repeated across a very long input).
- The actual fix: a Web Worker with a timeout. Matching does not run
on the same thread as the page you are looking at. It runs inside a same-origin Web
Worker — a background script this page starts, loaded from a file next to this one,
not a network request. The tester starts a timer the moment it sends a pattern to the
worker. If the worker has not replied by the deadline, the page calls
Worker#terminate(), which kills the worker outright — even though it is still stuck deep inside a nativeRegExp#execcall that will never return on its own — and a fresh worker is created for your next attempt. Terminating a worker does not touch the main thread at all, which is precisely why the rest of the page keeps responding to clicks and typing throughout. That is the property that actually matters: not that the runaway match finishes quickly (it may never finish), but that killing it is always possible and never blocks anything else.
There is a smaller, easier-to-fix trap in the same neighbourhood: a pattern that can
match a zero-length string with the global flag on, such as /x*/g against text
with no x in it. A naive loop that calls exec() and only relies on
the engine to advance its own position can match the same empty string at the same index
forever. This page’s matching loop checks the length of every match and manually
advances lastIndex by one whenever a match is zero-length, which is the
standard, documented way to make that loop terminate.
Explaining a pattern in plain English
The explainer is a second, completely separate pass over the pattern’s text — it never compiles or runs the pattern, so it carries none of the risk described above, no matter how pathological the pattern looks. It walks the pattern character by character, recognising anchors, character classes and their shorthand forms, groups (capturing, non-capturing, named, lookahead, lookbehind), backreferences, and the four quantifier shapes with their greedy and lazy variants, and produces one row per token: the raw text on the left, a sentence on the right. It is a hand-written scanner rather than a full parser, so a handful of exotic constructs get a generic description instead of a precise one — an accepted trade-off for explaining the common 95% of patterns clearly, which is what a cookbook is for.
Substitution
The replace tab supports the same template syntax JavaScript’s own
String.prototype.replace understands: $& for the whole match,
$1–$99 for numbered groups, $<name> for
named groups, and $$ for a literal dollar sign. It is reimplemented here rather
than calling .replace() directly so that substitution goes through the exact
same capped, manually-advanced matching loop as the Matches tab — one ReDoS story for
both features, not two, and both run inside the same worker with the same timeout.
Never an unhandled error, never raw HTML from your input
Two more things worth stating plainly. First: every operation on this page —
compiling, matching, explaining, substituting — is wrapped so that a bad pattern or
bad input produces a message, never a crash. Try [, ( or a bare
* in the pattern field and you get the engine’s own explanation, not a
blank page. Second: every value that reaches the screen from your pattern or your test
text — the highlighted matches, the match list, the explanation, the substitution
result — is written with textContent, never assembled into an HTML
string. Paste <img src=x onerror=alert(1)> as test text and it renders as
those literal characters, because there is no code path on this page that turns your text
into markup.
Worked examples
1. Pulling structured data out of free text with named groups
You have a log line like 2026-07-22 14:03:11 ERROR payment-service: timeout
after 5000ms and you want the date, the level and the service name, not just a
yes/no match. Named groups turn a single match into a small object:
^(?<date>\d{4}-\d{2}-\d{2}) (?<time>\d{2}:\d{2}:\d{2}) (?<level>[A-Z]+) (?<service>[\w-]+):
Paste that pattern here with the log line as test text and the match list shows four
named captures — date, time, level,
service — each with its own value, instead of one long numbered match
you would have to count positionally. In code, match.groups.service reads
far better than match[4], and it survives someone inserting a new group
earlier in the pattern without silently renumbering everything downstream.
2. Why .* is greedy, and when that bites you
Given the HTML fragment <b>bold</b> and <i>italic</i>,
the pattern <.*> does not match <b> the way most
people expect. * is greedy by default — it consumes as much as it can
and only gives characters back if the rest of the pattern cannot match otherwise —
so it grabs everything up to the last > in the string, matching
the entire fragment from <b> to </i> as one result.
Add ? after the quantifier to make it lazy — <.*?>
— and it takes the smallest match that still lets the rest of the pattern succeed,
so it stops at the first > it meets: four separate matches,
<b>, </b>, <i>,
</i>. Try both patterns here against the same text and watch the
highlight change from one long span to four short ones. The deeper lesson usually matters
more than the fix: a general .* across angle brackets is fragile on real HTML
regardless of greediness — nested tags, attributes containing >
inside a quoted string, and comments all break it. For anything beyond a fixed, known
fragment, parse the HTML with a real parser (the DOM, in a browser) rather than a regex.
3. A validator that is deliberately not exhaustive
The email pattern in the library, ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$,
rejects "quoted string"@example.com, which RFC 5322 technically allows, and
accepts a@b.co, which nothing stops you registering but which will bounce
every mail server ever built for it. Both are correct behaviour for this pattern’s
actual job: catching obvious typos in a signup form, not proving deliverability.
The only way to know an email address actually works is to send a message to it and
see what comes back — ideally a confirmation link the person has to click. Treat
every email regex, including this one, as a typo filter with a known false-negative rate
on the exotic 1% of technically-legal addresses, not as proof the address is real. The
dedicated email-validation page goes through the trade-off in more
detail, including why + in the local part deserves special handling in your
application code even after the regex accepts it.
Reference: tokens, quantifiers, anchors, classes and flags
Character classes
| Token | Matches |
|---|---|
. | Any character except a line break (unless the s flag is set) |
\d | A digit, 0–9 |
\D | Any character that is not a digit |
\w | A word character: letter, digit or underscore |
\W | Any character that is not a word character |
\s | A whitespace character: space, tab, newline and friends |
\S | Any character that is not whitespace |
[abc] | Any one of a, b or c |
[^abc] | Any character except a, b or c |
[a-z] | Any character in the range a to z |
Quantifiers
| Token | Meaning |
|---|---|
* | Zero or more, as many as possible (greedy) |
+ | One or more, as many as possible (greedy) |
? | Zero or one — makes the preceding token optional |
{n} | Exactly n times |
{n,} | n or more times |
{n,m} | Between n and m times, inclusive |
*? +? ?? | Lazy variants — as few repetitions as possible |
Anchors and boundaries
| Token | Meaning |
|---|---|
^ | Start of the string, or of a line with the m flag |
$ | End of the string, or of a line with the m flag |
\b | A word boundary — between a word character and a non-word character |
\B | Anywhere that is not a word boundary |
Groups and lookaround
| Token | Meaning |
|---|---|
(…) | Capturing group — remembers what it matched, numbered from 1 |
(?:…) | Non-capturing group — groups for the quantifier, but is not remembered |
(?<name>…) | Named capturing group |
(?=…) | Lookahead — must be followed by this, but it is not part of the match |
(?!…) | Negative lookahead — must NOT be followed by this |
(?<=…) | Lookbehind — must be preceded by this (Safari 16.4+) |
(?<!…) | Negative lookbehind — must NOT be preceded by this |
\1 | Backreference to capturing group 1 |
\k<name> | Backreference to the group named “name” |
| | Alternation — either the part on the left or the part on the right |
Flags
| Flag | Name | Effect |
|---|---|---|
g | Global | Find every match instead of stopping at the first |
i | Ignore case | Uppercase and lowercase letters match each other |
m | Multiline | ^ and $ also match at every line break |
s | Dot-all | . also matches line breaks |
u | Unicode | Treats the pattern as Unicode code points, not UTF-16 code units, and enables \p{…} |
y | Sticky | Only matches starting exactly at lastIndex, with no scanning forward |
Frequently asked questions
Is my pattern or test text sent anywhere?
No. Compiling, matching, explaining and substituting all run as JavaScript inside this tab — the matching step specifically runs inside a same-origin Web Worker, which is still just a background thread of this same page, not a network destination. There is no upload endpoint on this page and no code path capable of sending what you type anywhere.
You do not have to take that on faith. Open developer tools (F12, or ⌘⌥I on a Mac), select the Network panel, type a pattern and some test text, and watch the request list stay empty.
Does testing a pattern here send it anywhere?
No. This page only ever tests JavaScript’s own RegExp, which every
browser already ships, so there is nothing that needs a server round-trip — and there
is no save or share feature, so there is nowhere for a pattern to be stored even in
principle. Testing one flavour is a deliberate scope limit rather than an oversight: a
tester covering PCRE, .NET, Python or Go has to reach an engine the browser does not
have. If you are evaluating any online tool for sensitive input, the reliable check is
the same everywhere — open DevTools, watch the Network tab while you type, and see
for yourself what does and does not leave the page.
What is catastrophic backtracking and could my pattern trigger it?
It is what happens when a regex engine has to try an exponential number of ways to
match the same input, usually because a repeated group contains something that can
itself repeat — (a+)+, ([a-z]*)*,
(\d+){2,} are the classic shapes. Against the right adversarial input
— a long run of the repeated character followed by something that can never satisfy
the rest of the pattern — matching can take longer than the age of the universe on
ordinary hardware. See the full explanation and mitigation above; the
short version is that this page warns on the textbook shape and, more importantly, runs
matching in a background worker with a timeout so a hang here can never freeze the tab.
Why did my pattern match one fewer or one more time than I expected?
The two usual causes are the g flag and greediness. Without g,
a pattern only reports its first match, ever — toggle the g chip on to
see every occurrence. With greediness: an unqualified * or +
consumes as much as it can before backtracking, which can make one large match swallow
several things you expected to see as separate matches. Add ? after the
quantifier for the lazy variant, which takes the smallest match that still lets the rest
of the pattern succeed — see worked example 2 above for exactly this, with the
highlight to compare both ways.
My pattern works in Python or PHP but throws a SyntaxError here. Why?
Almost always a named-group spelling mismatch: Python’s re and PHP’s
PCRE both accept (?P<name>…), while JavaScript requires
(?<name>…) — no P. The second most common
cause is a possessive quantifier (a++) or an atomic group
((?>…)), neither of which JavaScript’s regex engine
implements at all. See flavour honesty above for the full list of
differences worth knowing before you copy a pattern across languages.
Can this validate against a full specification, like RFC 5322 for email?
No, and neither should you want it to for most purposes. A regex that fully implements RFC 5322’s email grammar is hundreds of characters long, accepts addresses no real mail server does in practice (quoted strings, comments, IP-literal domains), and still cannot tell you whether the mailbox exists. The library’s email pattern, like most production validators, targets the common 99% case — catching obvious typos — and treats an unreachable inbox as a problem for a confirmation email to catch, not a regex. See worked example 3 above.
Does the highlighting understand overlapping matches?
No — and neither does JavaScript’s own regex engine. Once a match consumes a run of characters, scanning continues from the end of that match, so a second match cannot start inside characters the first one already used. If you need overlapping matches (every position where a shorter pattern could start, even inside a longer match found first), you have to re-run the search starting one character after each previous start rather than after each previous end — JavaScript’s built-in matching does not offer this as an option, and neither does this page.
What happens with a zero-length match, like \b or x*?
It is reported like any other match, with identical start and end positions and an
empty matched string. The one thing that has to be handled carefully is advancing past
it: with the g flag, a naive loop that lets the engine manage its own
position can get stuck reporting the same empty match at the same index forever. This
page’s matching loop detects a zero-length match and advances one character past it
manually, which is the standard fix and the reason the match list here always
terminates.
Is the pattern explainer always exactly right?
For the constructs covered in the reference tables above — anchors, character classes, shorthands, the four quantifier shapes, groups, lookaround, backreferences — yes. It is a hand-written scanner rather than a complete regex parser, so a small number of exotic constructs (deeply nested Unicode property escapes, unusual flag combinations) fall back to a generic description rather than a precise one. That trade-off favours explaining the patterns people actually paste clearly, over parsing every legal but rare construct.
Does it work offline, once the page has loaded?
Yes. After the initial load — including the worker script, which is a same-origin file rather than a remote fetch — nothing further is requested. Save the page with Ctrl/⌘S and matching, explaining and substitution keep working with the network disconnected.
Use cases and limitations
What people use this for
- Validating form input — email shape, phone shape, a slug for a URL — before it reaches a server, as a first-line typo catcher, not a proof of correctness.
- Pulling fields out of semi-structured text — log lines, CSV-ish exports, config values — using named groups to get a small object back instead of a yes/no match.
- Bulk find-and-replace across text that follows a pattern but is not
identical, such as normalising date formats or redacting anything that looks like a phone
number, using the Replace tab’s
$1/$&/$<name>template. - Learning what an unfamiliar pattern does — paste a regex you found in someone else’s code into the Explain tab before you trust it in production.
- Checking a pattern is safe before shipping it — the catastrophic-backtracking warning catches the textbook danger shapes before a pattern goes anywhere near a server that might not have this page’s worker-timeout protection.
What it deliberately does not do
- Test PCRE, Python, .NET or any other flavour. One engine, the one already in your browser. See flavour honesty.
- Guarantee a pattern is ReDoS-free. The heuristic warning catches the textbook shape, not every possible catastrophic construct — the worker timeout is what actually protects the tab, on every pattern, whether or not the heuristic recognised it in advance.
- Parse HTML, JSON or any nested, recursive structure. Regex has no general answer to balanced nesting; that needs a real parser. Worked example 2 above walks through exactly this trap.
- Prove an email, phone number or URL is real. Syntax only — a validator that also confirmed deliverability or reachability would need to make an actual network request, which is out of scope for a page that runs entirely client-side.
- Find overlapping matches. Neither does JavaScript’s engine; see the FAQ above.
Honest limitations
The catastrophic-backtracking heuristic is advisory and incomplete by design — it recognises one well-known shape and will miss others, particularly ones hidden behind alternation or backreferences. The worker timeout is the real safety net and does not depend on the heuristic recognising anything. The plain-English explainer is a hand-written scanner, not a complete formal grammar for every legal JavaScript regex construct; it covers the constructs in the reference tables precisely and falls back to a generic description for the rest. Very large test strings (above roughly 200,000 characters) may be slow to highlight in full even when the pattern itself is safe, simply because building a match list and a highlighted view over that much text takes real work; the iteration cap keeps this bounded rather than unbounded, at the cost of a truncation notice on genuinely enormous inputs.