Reverse proxy

Envoy listeners, routes and clusters

How Envoy's listener, filter chain, route table and cluster model fits together, what xDS does, the full timeout ladder, and the response flag table for debugging.

· 13 min read · How we verify this

Key points

  • A request traverses listener, filter chain, network filters, HTTP connection manager, HTTP filters, route table, cluster, endpoint, and every one of those is separately configurable.
  • Envoy routes are matched in declaration order, first match wins, unlike nginx's longest-prefix location rules, so an over-broad early route silently shadows later ones.
  • The route timeout defaults to 15s and is not reset per retry, so per_try_timeout multiplied by the attempt count must fit inside it.
  • Response flags such as UH, UF, UO and URX in the access log identify the failing stage faster than any other Envoy artefact.

Envoy has one data path: a listener binds a socket, a filter chain is selected for the connection, network filters run, the HTTP connection manager (HCM) parses HTTP, HTTP filters run in order ending with the router, the route table picks a route, the route names a cluster, and the cluster load balances across endpoints. Everything else (retries, timeouts, circuit breakers, outlier ejection) hangs off one of those objects, and each can be delivered statically in the bootstrap file or dynamically over xDS. Naming the stage a request died in is most of the debugging, and the access log response flag names it for you.

The object model in one table#

ObjectDecidesMatched or keyed byxDS API
ListenerWhich socket and port to accept onaddress.socket_addressLDS
Filter chainWhich pipeline handles this connectionfilter_chain_match: SNI server names, transport protocol, source or destination IP, ALPNpart of LDS
Network filtersL4 behaviour (TCP proxy, TLS inspector, HCM)order in the listpart of LDS
HTTP connection managerHTTP codec, access logs, tracing, stream timeoutsone per filter chainpart of LDS
HTTP filtersPer-request logic (auth, rate limit, fault, router)order in the list, router must be lastpart of LDS
Route configurationVirtual host selection then route selection:authority against domains, then ordered match rulesRDS
ClusterUpstream pool, protocol, health, circuit breakersroute's cluster fieldCDS
EndpointsActual host:port members and their healthload_assignment or EDSEDS

If you come from nginx: the HCM is roughly server {}, the route configuration is the set of location {} blocks, and the cluster is upstream {}. The difference that matters is that the HCM is a network filter, so TLS termination, TCP proxying and HTTP proxying are just different filter stacks on the same listener.

What xDS is, and when static config is the right answer#

xDS is the family of discovery APIs Envoy uses to fetch configuration at runtime over streaming gRPC, REST polling, or a watched file. Each API supplies one layer of the object model.

APISupplies
LDSListeners, including filter chains and HCM config
RDSRoute configurations referenced by name from an HCM, the layer that changes most often
CDSClusters
EDSEndpoints for a cluster, usually from a service registry
SDSTLS certificates and validation contexts, allowing rotation without a restart
ADSAll of the above multiplexed on one gRPC stream so updates arrive in a defined order

ADS exists because of an ordering hazard: on separate streams Envoy can learn about a route pointing at a cluster it has not received yet, or drop endpoints for a cluster still in use. ADS serialises updates so the safe sequence (CDS, EDS, LDS, RDS on the way up, reverse on the way down) is possible. Use ADS unless you have a reason not to.

Static configuration is not a toy. A single static bootstrap is correct when the upstream set is fixed or discovered by DNS, because STRICT_DNS clusters already give you runtime endpoint changes with no control plane. Reach for xDS when endpoints, routes or certificates change faster than you want to redeploy a file.

Route matching, and where it differs from nginx#

Route selection is two steps. First Envoy picks a virtual host by matching the request :authority against each virtual host's domains list: exact match wins, then the longest suffix wildcard (*.example.com), then prefix wildcard, then the catch-all *. Then it walks that virtual host's routes array in order and takes the first entry whose match succeeds.

MatcherFieldNotes
Prefixmatch.prefixPlain string prefix, does not respect path segments
Path segment prefixmatch.path_separated_prefixMatches only on / boundaries, so /api does not match /apiary
Exact pathmatch.pathFull path minus query string
Regexmatch.safe_regexRE2 syntax, must match the whole path
Headermatch.headersstring_match (exact, prefix, suffix, contains, safe_regex), present_match, range_match, and invert_match
Querymatch.query_parametersstring_match or present_match
Methodmatch.headers on :methodPseudo-headers are matched like normal headers

prefix_rewrite replaces only the portion the prefix matched, so prefix: "/api/" with prefix_rewrite: "/" turns /api/users/7 into /users/7. regex_rewrite applies a RE2 pattern to the whole path with a substitution using \1 style references, for rewrites that are not a leading-segment swap. Either way Envoy records the pre-rewrite path in x-envoy-original-path, which is why the default log format prints %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%.

Cluster types, and STRICT_DNS versus LOGICAL_DNS#

TypeMembershipRe-resolution behaviourUse when
STATICExplicit IP:port list in the configNoneFixed IPs, sidecar to localhost
STRICT_DNSEvery address returned by DNS becomes a hostRe-resolves on dns_refresh_rate (default 5s) and updates the member setHeadless services, small to medium DNS-backed pools
LOGICAL_DNSOnly the first returned address is used, as a single logical hostRe-resolves on the same schedule, but existing connections are kept and only new connections use the newest addressVery large DNS pools, external endpoints behind a rotating record
EDSSupplied by the control planePush basedService mesh, anything with a registry
ORIGINAL_DSTDerived from the connection's original destinationPer connectionTransparent interception

The practical difference: STRICT_DNS holds a connection pool per resolved address, so a record with 200 A entries gives 200 pools per Envoy. LOGICAL_DNS holds one logical host, keeping memory and connection count flat but giving up per-endpoint load balancing and outlier ejection.

Circuit breakers, outlier detection and retries#

These three are different mechanisms and are commonly confused.

  • Circuit breakers are cluster-wide concurrency limits checked before dispatch: max_connections, max_pending_requests and max_requests default to 1024, max_retries to 3. Tripping one gives 503 with flag UO.
  • Outlier detection ejects individual endpoints on observed failures. Defaults: consecutive_5xx 5, interval 10s, base_ejection_time 30s, max_ejection_percent 10. Ejection time grows with repeat ejections.
  • Retries are per route: retry_on (5xx, gateway-error, reset, connect-failure, retriable-4xx, refused-stream, retriable-status-codes, envoy-ratelimited), num_retries, per_try_timeout, and exponential backoff with a 25ms base interval.

max_retries defaulting to 3 is a cluster-wide concurrency budget, not per request. Under a partial upstream failure the whole cluster is limited to three retries in flight and the rest fail immediately with URX. That is deliberate (it stops a retry storm finishing off a struggling backend), but it surprises people who set num_retries: 3 on a route and see almost no retries happen.

The timeout hierarchy#

SettingWhereDefaultScope
connect_timeoutCluster5s if unsetTCP and TLS handshake to one endpoint
timeoutRoute15sEntire request, from full request received to complete response, including all retries
per_try_timeoutRoute retry policyfalls back to the route timeoutOne attempt
stream_idle_timeoutHCM, overridable per route300sNo activity on the stream in either direction
request_timeoutHCMdisabledStart of stream until the complete request is received
idle_timeoutcommon_http_protocol_options1hConnection with no active streams

Streaming responses (server-sent events, long gRPC streams) need the route timeout set to 0s and stream_idle_timeout raised on that route specifically. Disabling it globally on the HCM removes your only protection against stuck streams.

Response flags: the fastest Envoy debugging artefact#

The default access log format includes %RESPONSE_FLAGS%. Each flag names the stage that ended the request.

FlagMeaningTypical statusFirst thing to check
UHNo healthy upstream in the cluster503Health checks, outlier ejections, empty EDS response
UFUpstream connection failure503connect_timeout, refused connections, security groups
UCUpstream closed the connection503Upstream keep-alive idle timeout shorter than Envoy's
UOUpstream overflow, a circuit breaker tripped503max_pending_requests, max_requests, max_connections
URUpstream sent a reset503Upstream crash, HTTP/2 GOAWAY or RST_STREAM
URXRetry limit exceeded or max connect attempts exceeded503Cluster max_retries budget, num_retries
UTUpstream request timeout504Route timeout or per_try_timeout
UPEUpstream protocol error502h2c versus HTTP/1.1 mismatch on the cluster
NRNo route matched404Virtual host domains, route order
NCCluster named by the route does not exist503CDS did not deliver it, or a name typo
DCDownstream connection terminatednone loggedClient hung up, or an upstream LB timeout in front of Envoy
DPEDownstream HTTP protocol error400Malformed request, protocol mismatch
DTConnection or stream exceeded its max durationvariesmax_connection_duration
SIStream idle timeout fired504stream_idle_timeout, long-polling or SSE routes
IHInvalid value in a strictly checked header400request_headers_to_add, strict header validation
RLRate limited by the HTTP rate limit filter429Local or global rate limit config
UAEXExternal authorization denied the request403ext_authz filter and its service

Add %RESPONSE_CODE_DETAILS% to the log format. It turns UF into something like upstream_reset_before_response_started{connection_failure}, and distinguishes via_upstream (the upstream really did return that 503) from an Envoy-generated local reply. Flag plus code details settles most "is it Envoy or the backend" arguments in one line, the same question 502 vs 503 vs 504 covers at the HTTP level.

Worked example: a complete static bootstrap#

This is a runnable v3 bootstrap that terminates plain HTTP on 10000, strips the /api prefix, and proxies to a DNS-resolved cluster with retries, circuit breakers and outlier detection.

yaml
admin:
  address:
    socket_address: { address: 127.0.0.1, port_value: 9901 }

static_resources:
  listeners:
  - name: listener_http
    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
          codec_type: AUTO
          use_remote_address: true
          request_timeout: 10s
          stream_idle_timeout: 300s
          access_log:
          - name: envoy.access_loggers.file
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
              path: /dev/stdout
              log_format:
                text_format_source:
                  inline_string: "%RESPONSE_CODE% %RESPONSE_FLAGS% %RESPONSE_CODE_DETAILS% %DURATION%ms %UPSTREAM_HOST% \"%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%\"\n"
          route_config:
            name: local_route
            virtual_hosts:
            - name: backend
              domains: ["*"]
              routes:
              - match: { path_separated_prefix: "/api" }
                route:
                  cluster: service_backend
                  prefix_rewrite: "/"
                  timeout: 3s
                  retry_policy:
                    retry_on: "connect-failure,reset,5xx"
                    num_retries: 2
                    per_try_timeout: 900ms
          http_filters:
          - name: envoy.filters.http.router
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

  clusters:
  - name: service_backend
    type: STRICT_DNS
    connect_timeout: 1s
    lb_policy: ROUND_ROBIN
    dns_lookup_family: V4_ONLY
    circuit_breakers:
      thresholds:
      - priority: DEFAULT
        max_connections: 512
        max_pending_requests: 256
        max_requests: 512
        max_retries: 16
    outlier_detection:
      consecutive_5xx: 5
      interval: 10s
      base_ejection_time: 30s
      max_ejection_percent: 20
    load_assignment:
      cluster_name: service_backend
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address: { address: backend.internal, port_value: 8080 }

Observable behaviour: GET /api/users/7 reaches the backend as GET /users/7, and GET /apiary returns 404 with NR because path_separated_prefix will not match a partial segment. If backend.internal refuses connections you get 503 with UF after 1s. If it accepts and hangs you get 503 with URX at roughly 1.8s (two attempts of 900ms), not 504 with UT, because the per-try timeout fires first. URX versus UT is how you tell "retries exhausted" from "total budget spent".

Failure modes#

  • 404 with NR on a request you are sure is configured. The virtual host did not match. Envoy matches :authority including the port when the client sends one, so domains: ["example.com"] does not match Host: example.com:8443. List both, or use *.
  • 503 with UH right after a deploy. The cluster exists but has no healthy members. Check the admin /clusters endpoint for health flags and ejection counters before blaming DNS. Outlier detection ejecting most of a two-host cluster is a classic, and max_ejection_percent is the guard. See health checks and upstream failover.
  • 502 with UPE on gRPC traffic. The cluster is speaking HTTP/1.1 to an h2c-only backend. Set explicit HTTP/2 upstream options on the cluster; details in gRPC through a reverse proxy.
  • Config accepted, traffic still wrong. The admin /config_dump endpoint shows the merged effective config, including anything xDS overwrote. Diff that, not the file you edited.
  • DC with no status code. The client or a load balancer in front disconnected first. If it lands on a round number of seconds, something upstream of Envoy has a shorter idle timeout than Envoy does.

Why pick Envoy over nginx#

Three reasons hold up, and the rest is taste. Config changes without a reload: xDS updates listeners, routes, clusters, endpoints and certificates in a running process, where nginx forks a new worker generation and long-lived connections keep the old one alive. Observability is an output, not an add-on: per-cluster and per-route counters, histograms, response flags and tracing propagation ship in the binary. HTTP/2, HTTP/3 and gRPC are native on both sides, including h2c upstream, gRPC health checks and JSON transcoding, with no third-party modules.

The cost is verbose config, a control plane for anything dynamic, and a larger footprint per instance. The reverse proxy comparison sets that against HAProxy, Caddy and Traefik.

Frequently asked questions#

What is the difference between STRICT_DNS and LOGICAL_DNS in Envoy?#

STRICT_DNS makes every address returned by DNS a separate host with its own connection pool, health state and load balancing weight. LOGICAL_DNS keeps a single logical host and only uses the first resolved address for new connections, which keeps connection counts flat for very large or rapidly changing DNS records but gives up per-endpoint load balancing and ejection.

What does response flag UH mean in Envoy access logs?#

UH means "no healthy upstream": the route matched and the cluster exists, but every endpoint in it is unhealthy, ejected by outlier detection, or absent. Envoy returns 503. Check the admin /clusters endpoint for member counts, health flags and outlier_detection ejection counters.

What is Envoy's default route timeout?#

The route timeout field defaults to 15 seconds. It covers the whole request from the moment the complete request is received to the moment the response is fully processed, and it is not reset between retry attempts, so per_try_timeout times the number of attempts has to fit inside it.

Why is my Envoy route not matching?#

The two usual causes are virtual host selection (the :authority header, including any port, did not match the domains list) and route ordering (an earlier, broader prefix route matched first, because Envoy takes the first match in array order rather than the longest prefix). Confirm with /config_dump and look for NR in the access log.

How do I enable WebSockets in Envoy?#

WebSocket upgrades are not enabled by default. Add an upgrade_configs entry with upgrade_type: websocket on the HTTP connection manager, or per route, and set the route timeout to 0s so a long-lived connection is not cut at 15 seconds.

What is the difference between circuit breakers and outlier detection?#

Circuit breakers are cluster-wide concurrency limits applied before dispatch (too many in-flight requests, pending requests, connections or retries) and produce flag UO. Outlier detection removes individual misbehaving endpoints from the load balancing set based on observed errors, showing up as shrinking healthy member counts and eventually UH.

Primary sources#

Every normative claim on this page is checked against the specification or the vendor documentation listed here. Where behaviour is version dependent, the version is named in the text.

  1. Envoy HTTP routing architecture overview
  2. Envoy HttpConnectionManager API (v3)
  3. Envoy route components API (v3)
  4. Envoy Cluster API (v3)
  5. Envoy circuit breaking
  6. Envoy outlier detection
  7. Envoy access logging usage and response flags
  8. Envoy dynamic configuration and xDS
  9. Envoy FAQ: why is my request timing out

Found something wrong, or behaviour that differs on your version? Report it with the version number and a primary source. Anything substantive is fixed in the page and logged on the corrections page. See editorial standards for how pages are researched, sourced and reviewed.

More in reverse proxy configuration#