skip to content

Inside an Envoy route_config, how does Envoy choose a virtual host and then a route for a request, and why can a routes entry with match: { prefix: "/" } placed first make every later entry unreachable?

level: middleimportance: should knowfreq 48%

answer

  1. two stages, two different rules
  2. domains: most specific wins
  3. routes: first match, strictly in order
  4. prefix "/" shadows everything below
  5. Envoy's own 404 means routing missed

basics

~20 s

Envoy first selects a virtual host by matching the request authority against its domains, most specific first, then scans that host's routes strictly in order and takes the first match. A leading prefix of "/" matches everything, so nothing after it is ever reached.

solid answer

~50 s

Two different matching rules apply in sequence. Choosing the **virtual host** is specificity-based: Envoy compares the request's `:authority` (or `Host`) against each `virtual_hosts` entry's `domains`, preferring an exact name, then the longest suffix wildcard like `*.example.com`, then a prefix wildcard, and only then a bare `*`. Choosing the **route** within that host is the opposite — `routes` is scanned strictly top to bottom and the **first** entry whose `match` succeeds wins. A `match: { prefix: "/" }` matches every path, so placing it first shadows everything below it; the ordering is the whole rule and Envoy will not reorder for you. A match can also require headers, query parameters or a runtime fraction, so specific routes generally belong above general ones. If no virtual host domain matches, or no route matches, Envoy generates a 404 itself — a 404 that no backend ever saw is the signature of a routing miss rather than an application bug.

code

yaml · 12 lines
yaml
virtual_hosts:
- name: app
  domains: ["api.example.com"]
  routes:
  - match: { path: "/healthz" }
    direct_response: { status: 200, body: { inline_string: "ok" } }
  - match: { prefix: "/api/", headers: [{ name: "x-canary", string_match: { exact: "true" } }] }
    route: { cluster: api_canary }
  - match: { prefix: "/api/" }
    route: { cluster: api_stable }
  - match: { prefix: "/" }
    route: { cluster: web }

go deeper

for a junior

Know that a request's Host or :authority picks the virtual host and the path picks the route, and that a route names the cluster that will serve the request.

for a middle

Explain the two different rules — most-specific domains, first-match routes — and show why a leading prefix of "/" shadows later entries. Know that Envoy itself generates the 404 when nothing matches.

for a senior

Treat route ordering as an operational risk: spot shadowed canary and header-matched routes, know that plain prefix matching is textual rather than segment-aware, and get prefix_rewrite trailing slashes right before they reach a backend.

for a principal

Decide who is allowed to author route order in a shared route configuration and how conflicts are prevented, since first-match semantics make a single broad entry a platform-wide outage vector regardless of who added it.

## Two matching stages, two different rules A `route_config` holds `virtual_hosts`; each virtual host holds `routes`. Requests pass through both, and the two stages behave differently — which is exactly the detail that trips people up. ### Stage 1: virtual host by authority, most specific wins Each virtual host has a `domains` list matched against the request authority — the `Host` header on HTTP/1.1 or the `:authority` pseudo-header on HTTP/2 and HTTP/3. Envoy prefers, in order: 1. an exact match (`api.example.com`), 2. the longest **suffix** wildcard (`*.example.com`), 3. the longest **prefix** wildcard (`api.*`), 4. the universal wildcard (`*`). Only one wildcard is allowed per domain entry and it must be at one end. A given domain may appear in only one virtual host in a route configuration; duplicates are a config error. Because selection is specificity-based, the order of virtual hosts in the list does not matter. If no virtual host matches the authority, Envoy answers with a 404 from the proxy itself. ### Stage 2: route by first match, in order Within the chosen virtual host, `routes` is an **ordered** list evaluated top to bottom, and the first entry whose `match` succeeds is used. There is no most-specific rule and no backtracking to another virtual host. The path matchers are: - `prefix` — a string prefix of the path; - `path` — the exact path; - `path_separated_prefix` — a prefix that must end on a `/` boundary, so `/api` does not match `/apifoo`; - `safe_regex` — a regular expression over the whole path. Any of these can be combined with `headers`, `query_parameters`, a `runtime_fraction` for percentage splits, and `case_sensitive`. ## Why a leading catch-all shadows everything `prefix: "/"` is true for every path. Placed first, it wins for every request and no later route is ever evaluated: ```yaml routes: - match: { prefix: "/" } # matches everything route: { cluster: web } - match: { prefix: "/api/" } # unreachable route: { cluster: api } ``` Envoy does not warn about this — an unreachable route is valid configuration, merely useless. Invert the order and both work. The general rule is **specific above general**, and the catch-all belongs last if it exists at all. The same shadowing happens more subtly with header matchers: a route matching only on `prefix: "/v1/"` placed above one matching `prefix: "/v1/"` *plus* a `x-canary` header means the canary route never fires, because the broader entry already matched. ## Prefix matching is textual `prefix: "/api"` also matches `/apifoo` and `/apiary`, because it is a plain string prefix with no notion of path segments. That is what `path_separated_prefix` is for. Teams that discover this the hard way usually find it as an authorization gap rather than a routing bug: a rule intended to cover one path tree quietly covers sibling paths that merely share a spelling. ## What a matching route can do The usual action is `route: { cluster: <name> }`, but the matched entry can also: - split across `weighted_clusters` with per-cluster weights; - rewrite the path with `prefix_rewrite`, or the authority with `host_rewrite_literal` or `auto_host_rewrite`; - return a `redirect` or a `direct_response` without contacting any backend at all. `prefix_rewrite` deserves care: it replaces exactly the matched prefix, so a route matching `/api/` rewriting to `/` turns `/api/users` into `/users` — and a mismatch between the trailing slashes of the match and the rewrite is the classic source of doubled or missing slashes upstream. ## Reading routing failures Because Envoy generates the 404 for both a domain miss and a route miss, the first diagnostic question is always whether the request even reached a virtual host. Check the authority the client actually sent — a client using an IP address or an unexpected port suffix will not match a `domains` entry written for the hostname. Then read the routes top to bottom the way Envoy does, and look for the first entry that could have matched. Nine times out of ten the surprising route is not misconfigured; it is simply below something broader.

  • Does the order of virtual_hosts entries matter the way the order of routes does?
    No. Virtual host selection is specificity-based on `domains`: exact name, then longest suffix wildcard, then prefix wildcard, then `*`. Order in the list is irrelevant and a domain may appear in only one virtual host. Route selection inside the chosen host is the opposite — strictly first-match in list order — which is why the two stages need to be reasoned about separately.
  • How would you send ten percent of traffic on one route to a different backend?
    Use `weighted_clusters` in the route action, listing both clusters with weights, so Envoy splits matching requests by weight. For a deterministic override you add a second, more specific route *above* it that matches a header and sends those requests entirely to the canary cluster — order matters, since the broader weighted route would otherwise match first.
  • A route matches prefix "/api" and rewrites it. Why do some upstream paths come out with a doubled slash?
    `prefix_rewrite` replaces exactly the matched prefix text, so trailing slashes must line up between the match and the rewrite. Matching `/api` and rewriting to `/` turns `/api/users` into `//users`. Matching `/api/` and rewriting to `/` yields `/users`. Also consider `path_separated_prefix`, since a plain `prefix: "/api"` matches `/apifoo` too.

saying these in an interview costs you the question

  • Thinks routes are chosen by longest or most specific match
  • Puts the catch-all route first
  • Assumes prefix matching respects path segments
  • Believes Envoy falls through to another virtual host
  • Reads a proxy-generated 404 as an application bug

context