Lesson 1
One route set, three different answers
Envoy, Kong and OpenResty all take a list of routes and pick one per request. It sounds like the same job, but the three define “matches” differently and order their priorities differently. This lesson builds one set of five routes on all three, sends the same seven paths through, and records which route wins.
The rig
Four containers on one Docker network. The backend is an nginx that echoes the path it received, so every route is configured to rewrite the path into its own marker — reading the response tells you which route won.
server {
listen 8080;
default_type text/plain;
location / { return 200 "$request_uri\n"; }
}
All three gateways get the same five routes:
| kind | pattern | |
|---|---|---|
| r1 | prefix | /a |
| r2 | exact | /a/b |
| r3 | prefix | /a/b/c |
| r4 | regex | /a/.*\.png$ |
| r5 | prefix | / |
Kong has no notion of an exact match, so r2 on Kong is a regex anchored at both ends. Versions measured: Envoy 1.31.10, Kong 3.9.3 running DB-less, OpenResty 1.31.1.1.
“Regex” means three different things
Before priority order there is a deeper difference: the same pattern string does not match the
same set of paths. Measured with a single regex route /b$ plus a catch-all:
| pattern | path | Envoy | Kong | OpenResty |
|---|---|---|---|---|
| /b$ | /b | match | match | match |
| /a/b | no | no | match | |
| /xb | no | no | no | |
| /b | /b | match | match | match |
| /b/c | no | match | match | |
| /a/b | no | no | match |
Those six rows are enough to read off three rules:
- Envoy —
safe_regexmust match the whole path. The pattern/bdoes not match/b/c. - Kong — the pattern after
~is anchored at the start./bmatches/b/cbut not/a/b. - OpenResty — like nginx, the regex is searched anywhere
in the path.
/b$matches/a/btoo.
/admin, stops matching
/admin/users when moved from nginx to Envoy; moved the other way it starts
catching /v1/admin as well. A migration has to rewrite the pattern for the
semantics of the destination and then retest it against the real paths.
Envoy sorts nothing
In Envoy the winner is the first route that matches, in written order. There is no sorting step at all. Writing the same five routes in three different orders gives three quite different tables:
| path | written r1→r5 | written r5→r1 | written r4,r3,r2,r1,r5 | written r2,r3,r4,r1,r5 |
|---|---|---|---|---|
| /a | r1 | r5 | r1 | r1 |
| /a/b | r1 | r5 | r2 | r2 |
| /a/b/c | r1 | r5 | r3 | r3 |
| /a/b/c/d | r1 | r5 | r3 | r3 |
| /a/x.png | r1 | r5 | r4 | r4 |
| /a/b/c/x.png | r1 | r5 | r4 | r3 |
| /z | r5 | r5 | r5 | r5 |
The second column is the worst case: writing the catch-all first makes the other four routes
dead, and Envoy starts normally without a single warning. The last column is
subtler: only because the prefix /a/b/c is written before the .png
regex does /a/b/c/x.png end up somewhere else.
The same experiment on Kong and OpenResty: written forwards or backwards, the table is identical. Both reorder the list before serving.
How Kong and nginx sort
With the five routes above, Kong and OpenResty agree row for row:
| path | winner | why |
|---|---|---|
| /a | r1 | only prefixes /a and / match; the longer one wins |
| /a/b | r2 | exact match (on Kong, a regex anchored at both ends) |
| /a/b/c | r3 | longest matching prefix |
| /a/b/c/d | r3 | longest matching prefix |
| /a/x.png | r4 | a regex beats a prefix |
| /a/b/c/x.png | r4 | a regex beats even a longer prefix |
| /z | r5 | only the catch-all is left |
The second-to-last row is the one worth keeping: a regex beats a prefix however long the
prefix is. Measured separately on Kong with the prefix /a/b/c/d/e against the
regex /a/.*\.png$: /a/b/c/d/e/x.png goes to the regex, and only
/a/b/c/d/e/x.txt falls back to the prefix. nginx behaves the same, except that
nginx has ^~ to suppress the regex step — Kong and Envoy have no equivalent.
~/a/b/.*$ and ~/a/.*\.png$ both match
/a/b/x.png. Measured under both written orders, Kong picked ~/a/b/.*$
each time, even though the other pattern is longer. So it follows neither the written order
nor the pattern length. Setting regex_priority: 10 on the .png
pattern does make it win — that is the only control there is, and it is worth setting
explicitly wherever two regexes can overlap.
Two identical routes
Write two routes with the same pattern /a, pointing at different backends:
| gateway | result |
|---|---|
| OpenResty | refuses to start: [emerg] duplicate location "/a" |
| Envoy | starts normally, the route written first serves |
| Kong | starts normally, and not the route written first serves |
That last row needs spelling out. Three pairs were measured: a route named k1
written before k2 gave k2; zz before aa
gave zz; and reversing that to aa before zz still gave
zz. So the winner follows the route name, not the written order — and
nobody picks a name intending to decide priority with it. Only nginx refuses to start; the
other two quietly choose one.
The lab
Type your own routes and the paths you want to try, then watch the three gateways answer. Any row where they disagree is highlighted. The algorithms are rewrites of all three and match 74 of 74 curl results from the rig above.
The routes
Which route wins
Takeaways
- “Regex” is three different things: Envoy matches the whole path, Kong anchors at the start, nginx searches anywhere.
- Envoy takes the first match in written order; Kong and nginx reorder, so written order does not matter to them.
- On Kong and nginx a regex beats a prefix, however long the prefix is.
- Between two regexes of equal
regex_priority, Kong's choice cannot be derived — setregex_priorityexplicitly. - Two routes with the same pattern: nginx refuses to start, Envoy serves the first written, Kong serves one chosen by name.
The next lesson asks the other half of the question: once a route has been chosen, what does the backend actually receive.