skip to content

Listeners, Filters & Clusters

This is Envoy's object model: a listener binds a port, a filter chain processes the bytes flowing through it, a route picks an upstream cluster, and the cluster holds the endpoints and load-balancing policy. I need this because every mesh config I will ever write is ultimately compiled down into these four objects, and interviewers use them to check whether I understand the proxy or only its wrapper.

on this pageshow

questions

6

In an Envoy http_connection_manager, http_filters is an ordered list. Why must envoy.filters.http.router be the last entry, and in what order do the filters actually see a request versus its response?

level: middleimportance: must knowfreq 52%

answer

  1. router is terminal, ends iteration
  2. config is rejected, not ignored
  3. request order in, reverse order out
  4. authn must precede authz
  5. per-route override, not per-route reorder

basics

~20 s

The router is Envoy's terminal HTTP filter: it sends the request upstream and ends the chain, so anything listed after it can never run. Envoy rejects such a config. Filters see requests in listed order and responses in reverse order.

solid answer

~50 s

`envoy.filters.http.router` is a **terminal** filter — it performs route selection, hands the request to the chosen cluster, and stops filter iteration. Any filter configured after it would never see a request, so Envoy treats that as a configuration error and rejects the listener, and it likewise rejects a chain with no terminal filter at the end. Order matters for everything before it too. On the way in, Envoy runs the **decoder** path in the order you listed the filters, so an authentication filter must appear before an authorization filter that consumes its output, and a filter that rewrites headers must come before whatever reads them. On the way back, the **encoder** path runs in reverse order, so the filter listed first is the last to touch the response — which is what you want for something that wraps the whole exchange, like a header-adding or logging filter. Any filter may also pause iteration and resume later, which is how `ext_authz` blocks a request while it calls out.

code

yaml · 9 lines
yaml
http_filters:
- name: envoy.filters.http.jwt_authn
  typed_config:
    "@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication
    providers: {}
    rules: []
- name: envoy.filters.http.router
  typed_config:
    "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

go deeper

for a junior

Remember that http_filters is an ordered list and that envoy.filters.http.router must be the final entry because it is what actually sends the request to a backend.

for a middle

Explain the terminal-filter rule, that Envoy rejects a config violating it, and that the decode path runs in listed order while the encode path runs in reverse — then give an ordering example such as authentication before authorization.

for a senior

Reason about ordering as a latency and blast-radius decision: which filters buffer or call out on the decode path, where a response-stamping filter must sit, and how per-route configuration exempts endpoints without a second listener.

for a principal

Own the policy for what belongs in the proxy's filter chain at all versus in the application or a dedicated service, and the review discipline that keeps a shared chain from accumulating per-team filters that every request must pay for.

## The HTTP filter chain Inside `envoy.filters.network.http_connection_manager` sits `http_filters`, an ordered list of per-request extensions. Common ones are `envoy.filters.http.jwt_authn`, `envoy.filters.http.ext_authz`, `envoy.filters.http.lua`, `envoy.filters.http.wasm`, `envoy.filters.http.cors`, and finally `envoy.filters.http.router`. Each entry has a `name` and a `typed_config`. This chain is *per request*, not per connection. Two requests multiplexed on one HTTP/2 connection each run the chain independently. ## Terminal filters Envoy classifies some filters as terminal: they consume the request and produce a response rather than passing it along. The router is the canonical one — it looks up the route, picks a cluster, opens or reuses an upstream connection and streams the request out. Because it ends iteration, a filter placed after it is unreachable code. Envoy does not fail silently here. Config that puts the router anywhere but last, or omits a terminal filter altogether, is rejected when the listener is loaded, with an error stating that the terminal filter must be the last filter in an HTTP filter chain. In a static config the process refuses to start; with a dynamically delivered config the update is rejected and the previous config keeps serving. Either way the fix is the same: the router goes last, always. ## Decode order and encode order Envoy's filter API has two paths: - The **decoder** path handles the request: headers, body, trailers travelling downstream-to-upstream. It runs filters in the order they appear in `http_filters`. - The **encoder** path handles the response travelling back. It runs the same filters in **reverse** order. So with a list of `[A, B, router]`, a request is seen by A, then B, then the router; the response is seen by the router's output, then B, then A. This symmetry is deliberate — it makes the chain behave like nested wrappers. A filter that stamps a response header, records timing, or normalises errors is usually placed **first** precisely so it is **last** on the way out and observes the final response after everything else has modified it. ```yaml http_filters: - name: envoy.filters.http.jwt_authn # 1st in, 3rd out - name: envoy.filters.http.ext_authz # 2nd in, 2nd out - name: envoy.filters.http.router # terminal — must be last ``` ## Pausing and resuming A filter is not obliged to hand the request straight on. It can stop iteration — the C++ API's `StopIteration` statuses — and resume later, which is exactly what happens when `ext_authz` makes a network call to an external authorization service, or when a filter buffers the request body before deciding. While paused, no later filter runs and the router has not been reached, so nothing has been sent upstream yet. A filter can also short-circuit entirely by generating a local reply, which is how a rejected authorization produces a 403 that no backend ever sees. This is also why filter ordering has a latency cost: each filter that must buffer a body or call out on the decode path adds to time-to-first-byte upstream. ## Ordering rules that come up in real configs - **Authentication before authorization.** A JWT filter that validates a token and exports its claims must precede the filter that makes an allow/deny decision from those claims. - **Mutation before consumption.** If a Lua or Wasm filter rewrites a header that a downstream-listed filter matches on, it must come first. - **CORS before anything that would reject the preflight**, since a preflight request carries no credentials. - **Router last**, unconditionally. ## Turning a filter off for one route The chain is fixed per listener, but not every route needs every filter. Virtual hosts, routes and weighted clusters accept `typed_per_filter_config`, a map keyed by filter name that overrides or disables a filter for that scope — the usual way to exempt a health endpoint from `ext_authz` without maintaining a second listener. This is scoping, not reordering: you cannot change filter *order* per route, only per-filter behaviour. ## What an interviewer is checking That you understand the chain is ordered, directional and terminated — not a bag of independent features. Candidates who have only used a control plane's abstractions often assume the proxy sorts filters sensibly for them; it does not, and the reverse encode order in particular surprises people the first time a response header they expected to see does not appear.

  • Where would you place a filter that must stamp a header onto every response, including responses generated by Envoy itself?
    First in the list. The encode path runs in reverse, so the first-listed filter is the last to touch the response and therefore sees the final version — including local replies produced by a filter further down, such as a 403 from `ext_authz`. Placing it late means it runs early on the way out and misses later modifications.
  • How do you exempt a single route, such as a health endpoint, from an ext_authz filter that is installed on the listener?
    Use `typed_per_filter_config` on the virtual host, route or weighted cluster, keyed by the filter's name, to disable or override that filter for the scope. The filter chain itself stays identical for every request on the listener; only the per-scope configuration differs. You cannot reorder or remove filters per route.
  • An ext_authz filter is waiting on its external authorization service. What is happening to the request meanwhile?
    The filter has stopped iteration, so no later filter has run and the router has not been reached — nothing has been sent upstream. The downstream connection stays open and the request is held in Envoy. When the authorization response arrives, iteration resumes, or the filter generates a local reply such as a 403 and the request never reaches a backend at all.

saying these in an interview costs you the question

  • Assumes Envoy reorders filters for you
  • Thinks responses traverse filters in listed order
  • Puts an authorization filter before authentication
  • Believes filters after the router just get skipped quietly
  • Tries to reorder filters per route

context

open as a page

Trace a single HTTP request through Envoy's configuration objects, from the TCP port it arrives on to the backend instance that finally serves it. Which object handles each step, and what does each one own?

level: middleimportance: must knowfreq 78%

basics

~20 s

A listener binds the port and selects a filter chain; the http_connection_manager network filter parses HTTP and runs the HTTP filters; its route_config picks a virtual host and a route that names a cluster; the cluster load-balances across its endpoints.

open as a page

In Envoy's terminology and configuration, what do the words 'downstream' and 'upstream' refer to, and which Envoy objects sit on each side?

level: juniorimportance: should knowfreq 55%

basics

~20 s

In Envoy, downstream is the client that connects to Envoy and upstream is the service Envoy connects out to. Listeners and their filter chains are the downstream side; clusters and their endpoints are the upstream side.

open as a page

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%

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.

open as a page

An Envoy cluster with type: LOGICAL_DNS points at a DNS name that resolves to many backend addresses, yet almost all traffic lands on one backend. What does LOGICAL_DNS do that STRICT_DNS does not, and which would you use here?

level: seniorimportance: should knowfreq 35%

basics

~20 s

LOGICAL_DNS keeps a single logical host built from one resolved address, so the load balancer has exactly one endpoint to choose. STRICT_DNS keeps every address returned by DNS as a separate host and balances across all of them, which is what a multi-backend name needs.

open as a page

A single Envoy listener on port 443 carries several filter_chains with different filter_chain_match blocks. How does Envoy decide which chain handles an incoming connection, and what has to happen before it can match on the TLS server name?

level: seniorimportance: nice to knowfreq 30%

basics

~20 s

Envoy picks the single most specific matching filter chain, comparing criteria in a fixed priority order rather than taking the first that matches. Matching on SNI requires a listener filter to read the TLS ClientHello before selection happens.

open as a page