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?
answer
- four objects, downstream to upstream
- port, then bytes, then request, then backend
- http_connection_manager makes it L7
- route names a cluster by string
- cluster owns endpoints and lb_policy
basics
~20 sA 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.
solid answer
~50 sEnvoy's object model is four layers deep. A **listener** binds an address and port and accepts the connection; optional `listener_filters` sniff the first bytes, then a **filter chain** is selected and its **network filters** run. For HTTP traffic the network filter is `envoy.filters.network.http_connection_manager` (HCM): it terminates the connection's HTTP codec and turns bytes into requests and headers. HCM then runs its ordered **HTTP filters** — things like `ext_authz`, `lua`, `wasm` — ending in `envoy.filters.http.router`. The router consults the **route_config**: it picks a `virtual_hosts` entry by matching the request's authority against `domains`, then the first matching `routes` entry, which names a **cluster** by string name. The **cluster** owns the endpoint list, connection settings and the `lb_policy`, and its load balancer picks one endpoint to send the request to. The route names the cluster; the cluster owns the backends — that indirection is the whole point.
code
yaml · 35 linesstatic_resources:
listeners:
- name: main
address:
socket_address: { address: 0.0.0.0, port_value: 10000 }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: backend
domains: ["*"]
routes:
- match: { prefix: "/" }
route: { cluster: service_backend }
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: service_backend
connect_timeout: 0.25s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: service_backend
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address: { address: backend.internal, port_value: 8080 }go deeper
Be able to name the four objects in order — listener, filter chain, route, cluster — and say that a listener takes traffic in while a cluster sends traffic out.
Explain what each object owns and, crucially, that a route names a cluster by string while the cluster owns the endpoints and load-balancing policy. Know that http_connection_manager is the network filter that makes Envoy L7.
Use the model diagnostically: map a symptom (connection refused, self-generated 404, 503) onto the object that produced it, and explain why endpoint churn and routing changes are deliberately kept in separate objects.
Own the argument for why every mesh and gateway abstraction compiles down to these four objects, and what that means for choosing a control plane, for config blast radius, and for how much product logic you are willing to push into the proxy layer.
## The four objects Everything Envoy does — whether hand-written YAML or config generated by a control plane — reduces to four object types: **listeners**, **filter chains (with filters)**, **routes**, and **clusters**. Knowing the boundary between them is what lets you debug a proxy you did not configure yourself. ## 1. The listener: where bytes arrive A listener binds a socket, usually an `address.socket_address` with an `address` and a `port_value`. It is a *downstream* object: it accepts connections from clients. A listener may first run `listener_filters`, which peek at the beginning of the connection before any real processing — the TLS inspector reads the ClientHello for SNI and ALPN, the HTTP inspector detects the protocol version. Their job is to produce facts that the next step can match on. The listener then chooses one of its `filter_chains`. Each chain may carry a `filter_chain_match` (destination port, `server_names`, `transport_protocol`, `application_protocols`, source IP) and its own `transport_socket` — which is where TLS actually terminates, if it terminates here at all. ## 2. The filter chain: network filters A chain's `filters` are **network (L4) filters**. They see a byte stream, not requests. `envoy.filters.network.tcp_proxy` simply shovels bytes to a cluster — that is an L4 proxy and there is no route object involved at all. For HTTP you instead install `envoy.filters.network.http_connection_manager`. ## 3. http_connection_manager: bytes become requests HCM is the object that makes Envoy an L7 proxy. It owns the downstream codec (HTTP/1.1, HTTP/2, and HTTP/3 where enabled), request framing, `stat_prefix`, header handling such as `use_remote_address`, and the **HTTP filter chain**. HTTP filters operate on headers, body and trailers of one request: `envoy.filters.http.ext_authz` calls an external authorization service, `envoy.filters.http.lua` and `envoy.filters.http.wasm` run custom logic, and `envoy.filters.http.router` is the terminal filter that actually forwards the request upstream. Filters before the router can mutate, reject or pause a request; nothing after the router runs, because the router ends the chain. ## 4. route_config: choosing a destination *name* HCM carries either an inline `route_config` or a reference resolved dynamically. A route configuration holds `virtual_hosts`; each has a `domains` list matched against the request authority (the `Host` header or `:authority` pseudo-header), and an ordered `routes` list. Each route has a `match` (`prefix`, `path`, `safe_regex`, plus optional header and query matchers) and an action — most often `route: { cluster: <name> }`, but also `weighted_clusters`, `redirect` or `direct_response`. The critical detail: the route names a cluster **by string**. It does not know an IP, a port, or how many backends exist. ## 5. The cluster: the upstream group A cluster is the *upstream* object. It owns: - how membership is discovered — the `type` field: `STATIC`, `STRICT_DNS`, `LOGICAL_DNS`, `EDS`, `ORIGINAL_DST`; - the endpoints themselves, via `load_assignment` for static and DNS clusters; - `lb_policy` (`ROUND_ROBIN`, `LEAST_REQUEST`, `RING_HASH`, `MAGLEV`, `RANDOM`); - connection behaviour such as `connect_timeout`, upstream protocol options and TLS to the backend via its own `transport_socket`. When the router hands the request to a cluster, the cluster's load balancer picks one healthy endpoint and the request goes out on a connection from that cluster's pool. ```yaml routes: - match: { prefix: "/api/" } route: { cluster: service_backend } # a NAME, not an address ``` ## Why the split is designed this way Routes change for product reasons (a new path, a header-based split); cluster membership changes constantly for infrastructure reasons (a pod restarted, an instance scaled in). Keeping them in separate objects means endpoint churn never rewrites your routing rules, and route changes never disturb connection pools. It is also why the two are delivered by different discovery services. ## Reading a failure through the model The model tells you where to look. Connection refused at the socket means listener or filter-chain selection. A 404 that Envoy generated itself means no virtual host domain or no route matched. A 503 means routing succeeded but the cluster had no healthy endpoint or the upstream connection failed. Each symptom belongs to exactly one object.
- At which point in that path does Envoy stop dealing in bytes and start dealing in HTTP requests?At `http_connection_manager`. Everything before it — listener filters, filter-chain selection, other network filters — sees only a byte stream and connection-level facts such as SNI or source IP. HCM installs the codec that frames those bytes into requests, which is why anything path-, header- or method-based must live in HCM's HTTP filters or its route configuration, never in a network filter.
- A route points at a cluster name that does not exist. What happens?It depends on where the route configuration came from. A `RouteConfiguration` has a `validate_clusters` field that defaults to true for statically configured routes, so Envoy rejects the config at load time with an unknown-cluster error. For routes delivered dynamically it defaults to false, so the config is accepted and matching requests fail at runtime with a 503 because no cluster could be selected.
- If you configure only a tcp_proxy network filter instead of http_connection_manager, which of the four objects disappear?Routes and HTTP filters. `envoy.filters.network.tcp_proxy` names its destination cluster directly in its own config, so there is no `route_config`, no virtual hosts and no per-request matching — the whole connection goes to one cluster. Listeners, filter chains and clusters still apply. That is exactly the L4-versus-L7 trade expressed in Envoy's object model.
saying these in an interview costs you the question
- Thinks a listener maps one-to-one to a backend service
- Says the cluster performs path or header routing
- Calls http_connection_manager an HTTP filter
- Believes routes contain the backend IP addresses
- Uses upstream to mean the connecting client