NAME
Router::Ragel - High-performance URL router built on a Ragel-generated state machine
SYNOPSIS
use Router::Ragel;
my $router = Router::Ragel->new
->add('/users', 'users_list')
->add('/users/:id<int>', 'user_show')
->add('/blog/:year<int>/:month<int>/:slug', 'blog_post')
->compile;
my ($handler, @captures) = $router->match('/users/42');
# ('user_show', '42')
my @no_match = $router->match('/nope');
# ()
# Function form, faster than method dispatch:
my ($h, @c) = Router::Ragel::match($router, '/users/42');
DESCRIPTION
Router::Ragel compiles a set of URL patterns into a single Ragel finite-state machine and emits it as C via Inline::C. Matching a path is a fixed-cost walk over a DFA: there is no per-route loop and no regex engine. For applications with many routes or high request rates this typically beats regex- and trie-based routers by a wide margin (see "PERFORMANCE").
The router supports:
- Static segments (
/users,/products) - Named placeholders (
/users/:id,/blog/:year/:month) - Typed placeholders (
/users/:id<int>,/code/:c<[0-9]{4}>) - Inline placeholders mixed with literal text in a single segment
(
/v/:major<int>.:minor<int>) - Multiple independent router instances in the same process
METHODS
new
my $router = Router::Ragel->new;
Constructs a new router. Takes no arguments.
add
$router->add($pattern, $data);
Registers a route. $pattern is a path string (see "ROUTE PATTERNS");
$data is the value returned by match on a hit and may be any scalar.
Returns the router, so calls can be chained.
Adding a route after compile invalidates the compiled state; the next
match will croak until compile is called again.
compile
$router->compile;
Builds and binds the Ragel state machine. Must be called before match.
Returns the router. May be called more than once to incorporate routes added
between calls; see "LIMITATIONS" for the cost of recompiling.
match
my ($data, @captures) = $router->match($path);
Matches $path against the compiled routes and returns the route data
followed by captured values, in pattern order. Returns the empty list on no
match.
For the lowest call overhead, invoke match as a plain function and skip
Perl's method dispatch:
my ($data, @captures) = Router::Ragel::match($router, $path);
Both forms run the same compiled state machine.
ROUTE PATTERNS
A pattern is a string starting with /. Each segment between slashes is
either literal text or contains one or more placeholders. A placeholder is
:NAME optionally followed by a type constraint <TYPE>.
Placeholder names
The name is a run of word characters (\w+): letters, digits, and
underscores. With no type, a placeholder matches any non-slash bytes
([^/]+).
The name is greedy, so to follow a placeholder with literal characters that could otherwise extend the name, terminate the name with an explicit type:
/:type_extra # one placeholder named "type_extra"
/:type<string>_extra # placeholder "type" then literal "_extra"
Type constraints
Built-in aliases:
int=[0-9]+string=[^/]+(default; explicit form)hex=[0-9a-fA-F]+
Anything else inside <...> is passed verbatim to Ragel, so arbitrary
character classes and quantifiers work:
/code/:c<[0-9]{4}> # exactly four digits
/file/:name<[a-z0-9\-]+> # slug-like (escape '-' inside a class)
The dialect is Ragel's, not Perl/PCRE. Available: character classes,
quantifiers (*, +, ?, {n}, {n,m}), alternation, grouping, and
Ragel keywords (digit, alpha, alnum, xdigit, lower, upper,
space, punct, print, ascii, any). Not available: Perl
shortcuts (\d, \w, \s), anchors, lookaround, and backreferences.
Anchors are unnecessary anyway: segment boundaries are implicit.
A literal > cannot appear inside a <type> expression (the
parser closes the type at the first >); a literal < is
rejected at compile time. A literal - inside a character class must
be escaped as \- (Ragel parses an unescaped - as a range
operator and errors out, even at the start or end of the class). For any of
these, match a permissive class and post-process in user code.
Captures
Captures are returned positionally by match, in the order their
placeholders appear in the pattern. Placeholder names are not used at match
time.
Examples
/users # static
/users/:id # untyped placeholder, matches any non-slash
/users/:id<int> # typed: digits only
/blog/:year<int>/:month<int> # multiple typed placeholders
/v/:major<int>.:minor<int> # multiple placeholders in one segment
/file/:name<[a-z0-9\-]+>.:ext<[a-z]+> # inline + raw character classes
/path/to_:type<string>/id_:id<int>/end # mixed literals and placeholders
Caveats
Any : inside a segment introduces a placeholder; there is no escaping
mechanism. Avoid literal colons in path segments.
DEPLOYMENT
The compiled Ragel machine lives in a shared library that Inline::C
dlopens into the process; the function pointer is stored on the router
object. To avoid every worker compiling its own copy, call compile
once in the parent process before forking:
# in app.psgi or equivalent startup code
my $router = MyApp->build_router; # calls Router::Ragel->compile
# then exec the server with --preload-app or equivalent so children
# inherit the loaded .so via copy-on-write
If the cache is cold and several workers reach compile concurrently,
Inline::C serializes them on a directory lock and only one process runs
the C compiler -- the rest dlopen the resulting .so. That avoids the
cc/ragel stampede but every worker still pays the wait. Compiling in
the parent before fork eliminates the wait too.
For deterministic startup, populate the Inline::C cache at build/deploy
time and ship the warmed directory with the artifact (e.g., bake _Inline/
into your Docker image). Pin the cache location for reproducibility:
use Inline Config => DIRECTORY => '/opt/myapp/inline';
use Router::Ragel;
The use Inline Config line must be evaluated before Router::Ragel is
loaded (place it in the same file above use Router::Ragel, or in a
BEGIN block that runs first). Once Router::Ragel is loaded, the cache
location is fixed.
The compiled .so is architecture- and Perl-version-specific; build the
cache on the same target as production.
PERFORMANCE
Indicative numbers from eg/bench.pl (Linux x86_64, single core; matches
per second across seven mixed routes and six paths):
Rate Mojo R3(method) R3(fun) XS(fun) UR(method) UR(fun) Ragel(method) Ragel(fun)
Mojo 9166/s -- -97% -97% -99% -99% -99% -99% -99%
R3(method) 319956/s 3391% -- -4% -55% -56% -60% -68% -74%
R3(fun) 332160/s 3524% 4% -- -53% -55% -59% -66% -73%
XS(fun) 713678/s 7686% 123% 115% -- -3% -11% -28% -42%
UR(method) 735486/s 7924% 130% 121% 3% -- -9% -26% -41%
UR(fun) 804357/s 8675% 151% 142% 13% 9% -- -19% -35%
Ragel(method) 988728/s 10687% 209% 198% 39% 34% 23% -- -20%
Ragel(fun) 1238754/s 13414% 287% 273% 74% 68% 54% 25% --
Run eg/bench.pl to reproduce locally; routers that aren't installed are
skipped.
LIMITATIONS
- Patterns and paths are byte strings. Wide-character (utf8-flagged) strings are matched against their UTF-8 byte representation.
- Patterns must start with
/and must not be empty, contain a NUL byte, contain consecutive slashes, use a bare:placeholder name, or contain an empty or unterminated<type>.compilecroaks on any of these. - Matching is exact:
/usersmatches only/users, not//users,/users/, or/users//./usersand/users/are distinct routes. Normalize input ahead ofmatchto fold repeated or trailing slashes. - Adding a route after
compileinvalidates the compiled matcher;matchcroaks until you re-runcompile. Eachcompiledlopens a new shared library; the previous one stays loaded for the lifetime of the process. See "DEPLOYMENT" for the implications under pre-forking servers. - Calling
matchbeforecompilecroaks.
SEE ALSO
AUTHOR
vividsnow
LICENSE AND COPYRIGHT
Copyright (c) 2026 vividsnow.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.