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:

kindpattern
r1prefix/a
r2exact/a/b
r3prefix/a/b/c
r4regex/a/.*\.png$
r5prefix/

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:

patternpathEnvoyKongOpenResty
/b$/bmatchmatchmatch
/a/bnonomatch
/xbnonono
/b/bmatchmatchmatch
/b/cnomatchmatch
/a/bnonomatch

Those six rows are enough to read off three rules:

Copying a regex from one gateway to another is wrong by default This is not a detail. A pattern guarding access, say /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:

pathwritten r1→r5written r5→r1written r4,r3,r2,r1,r5written r2,r3,r4,r1,r5
/ar1r5r1r1
/a/br1r5r2r2
/a/b/cr1r5r3r3
/a/b/c/dr1r5r3r3
/a/x.pngr1r5r4r4
/a/b/c/x.pngr1r5r4r3
/zr5r5r5r5

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:

pathwinnerwhy
/ar1only prefixes /a and / match; the longer one wins
/a/br2exact match (on Kong, a regex anchored at both ends)
/a/b/cr3longest matching prefix
/a/b/c/dr3longest matching prefix
/a/x.pngr4a regex beats a prefix
/a/b/c/x.pngr4a regex beats even a longer prefix
/zr5only 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.

Between two regexes, which one Kong picks is not something you can work out The routes ~/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:

gatewayresult
OpenRestyrefuses to start: [emerg] duplicate location "/a"
Envoystarts normally, the route written first serves
Kongstarts 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

      The next lesson asks the other half of the question: once a route has been chosen, what does the backend actually receive.