A regex engine for Go that can't be tricked into hanging.
Most regex engines — the ones in Python, JavaScript, Java, Ruby, PCRE — work by
backtracking. It's a clever trick, and it's why they can do fancy things like
backreferences. It's also why a pattern as harmless-looking as (a+)+$ can bring
a production server to its knees. Feed it the wrong string and it doesn't just
get slow, it gets exponentially slow. People have taken down real services this
way. It even has a name: ReDoS.
flare takes a different road. It doesn't backtrack at all. Under the hood it runs your pattern as a state machine that walks the input exactly once, so matching is always linear in the length of the text. There is no evil input. There is no pattern that blows up. You can hand it a regex from a random user on the internet and go to sleep.
Here's the same pattern, (a+)+$, given a string of as that doesn't quite
match:
Python's re → 30 characters → still running after 10 seconds ✗
flare → 100,000 chars → done in 0.02 seconds ✓
That's not a tuned benchmark. That's just what happens.
go get github.com/umudhasanli/flare-regexGo 1.21 or newer. No dependencies — it's the standard library and nothing else.
package main
import (
"fmt"
"github.com/umudhasanli/flare-regex"
)
func main() {
re := flare.MustCompile(`(\d{4})-(\d{2})-(\d{2})`)
fmt.Println(re.MatchString("today is 2026-07-10")) // true
fmt.Println(re.FindString("today is 2026-07-10")) // 2026-07-10
fmt.Println(re.FindStringSubmatch("2026-07-10")) // [2026-07-10 2026 07 10]
fmt.Println(re.ReplaceAllString("2026-07-10", "$3/$2/$1")) // 10/07/2026
}If you've used Go's standard regexp package, this will feel like home. That's on
purpose — the method names line up so you can try flare without relearning
anything.
Nobody enjoys reading ^\d{1,3}(\.\d{1,3}){3}$ six months after they wrote it.
So flare ships a builder that reads like a sentence:
// Matches an IPv4 address like 192.168.0.1
re := flare.New().
Start().
Digit().Between(1, 3).
Group(func(b *flare.Builder) {
b.Lit(".").Digit().Between(1, 3)
}).Times(3).
End().
MustCompile()
re.MatchString("192.168.0.1") // trueLit escapes metacharacters for you, so New().Lit("a.b") looks for the literal
text a.b — not "a, then anything, then b". One less thing to get wrong.
Everyone has stared at a regex someone else wrote and had no idea what it does. flare can just tell you — in plain English:
re := flare.MustCompile(`^(cat|dog)s?\d+$`)
fmt.Print(re.Explain())• the start of the text
• capturing group #1:
• one of:
• the text "cat"
• the text "dog"
• the character "s", optional (zero or one time)
• a digit (0-9), repeated one or more times
• the end of the text
The standard library can run your pattern, but it can't describe it. flare keeps the parsed structure around, so it walks the pattern and turns it back into readable steps. It works on the command line too:
flare -explain '(\d{4})-(\d{2})-(\d{2})'Between the Builder (English → regex) and Explain (regex → English), you never have to hold a cryptic pattern in your head again.
Real patterns need real features. Name your groups and pull them out by name:
re := flare.MustCompile(`(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})`)
m := re.FindStringSubmatchMap("2026-07-10")
// map[year:2026 month:07 day:10]Match without caring about case, either for the whole pattern or just part of it:
flare.MustCompile(`(?i)hello`).MatchString("HELLO") // true
flare.MustCompile(`(?i:cat)dog`).MatchString("CATdog") // trueAnd when you're working with []byte, skip the string conversion entirely —
Match, Find, FindSubmatch, FindAll, and ReplaceAll all take bytes.
Installing the module also gives you a little command-line tool. It behaves like grep, but it's backed by the same engine, so no pattern anyone types will ever hang it.
go install github.com/umudhasanli/flare-regex/cmd/flare@latestflare '^error:' server.log # lines that match
flare -o '\d{3}-\d{4}' contacts.txt # only the matching parts
flare -n 'TODO' *.go # with line numbers
flare -c 'WARN' app.log # just count them
flare -r '$3/$2/$1' '(\d{4})-(\d{2})-(\d{2})' dates.txt # find and replace
cat access.log | flare '4\d\d' # reads stdin too| Literals | abc |
| Any character | . |
| Character classes | [a-z0-9], [^aeiou] |
| Shorthands | \d \D \w \W \s \S |
| Anchors | ^, $, \b, \B |
| Alternation | cat|dog|bird |
| Groups | (...), (?:...), (?P<name>...) |
| Case-insensitive | (?i) and (?i:...) |
| Repetition | * + ? {3} {2,} {2,5} |
| Lazy repetition | *? +? ?? |
| Escapes | \. \* \n \t … |
Backreferences (\1) and lookahead/lookbehind ((?=...)).
This isn't laziness — it's the whole point. Those are exactly the features that force an engine to backtrack, and backtracking is the thing that makes regex dangerous. Leaving them out is what lets flare promise it will never hang. It's a trade, and for the huge majority of real-world patterns, it's a trade worth making.
Three steps, and the whole engine is small enough to read in an afternoon:
- A recursive-descent parser turns your pattern into a small syntax tree.
- A compiler flattens that tree into a list of simple instructions — every
*,+, or|becomes a branch. - A virtual machine runs all the possible branches at the same time, keeping a set of active states and advancing them one character at a time.
Because the set of active states can never be bigger than the program itself, the work per character is bounded. Multiply by the number of characters and you get linear time. There's simply no room for an explosion to happen.
This is the same idea behind Go's own regexp package and Google's RE2, described
beautifully in Russ Cox's articles on the subject.
flare is a small, readable, dependency-free take on it.
Correctness: flare is fuzz-tested against Go's standard regexp on a set of
shared patterns. It's checked over tens of thousands of random inputs that both
engines agree on every match. Run it yourself:
go test -fuzz FuzzAgainstStdlib -fuzztime 30sPerformance: be clear-eyed here. Go's standard library regexp is also a
linear-time engine, and on ordinary patterns it is faster than flare and does
zero allocations. flare converts to runes and tracks submatch state, so it trades
some speed for its features. If you're on Go and want raw throughput on trusted
patterns, use the standard library.
So when should you reach for flare? When you value what it adds: the readable
Builder, Explain(), named-group maps, and a small self-contained engine you can
actually read and learn from. The linear-time guarantee matters most when you're
comparing against the backtracking engines in other languages — that's where
"can't be DoS'd" is a feature you don't otherwise get.
This project is meant to be approachable. If you've ever wanted to understand how a regex engine actually works, this is a good place to poke around — the code is short and there's no magic hiding in a dependency.
Good first contributions:
- More syntax: Unicode character classes, POSIX classes (
[[:alpha:]]),\A/\z - A UTF-8 fast path that avoids the
[]runeconversion (a real speed win) - Reducing per-match allocations in the VM
- More CLI flags (
-ifor case-insensitive, recursive directory search)
Open an issue to talk it through, or just send a PR. All welcome.
MIT. Do what you like with it — see LICENSE.
If flare saved you from a regex that hangs, consider giving it a ⭐.
It genuinely helps other people find it.