Tools Used in SRE · Envoy

Envoy

Reliability patterns taught circuit breakers, retries with backoff and jitter, timeouts, and bulkheads as things a service builds into its own code — Resilience4j in Java, Polly in .NET, a hand-rolled retry loop everywhere else. Envoy is a high-performance L3/L4/L7 proxy, originally built at Lyft and open sourced in 2016, that implements nearly that entire toolkit as network-layer primitives configured declaratively instead of coded per service. It became the CNCF's second graduated project, after Kubernetes, in November 2018 — and it's also the reason "service mesh" became a mainstream idea at all: Istio, Consul Connect, and most of the CNCF mesh ecosystem are a control plane wrapped around Envoy's data plane, not a competing proxy implementation.

☺ Explain it like I'm 10

Imagine a restaurant chain where every waiter carries a personal rulebook for what to do when the kitchen runs out of an ingredient — how many times to ask again, how long to wait before giving up, which cook to stop sending orders to after three burnt dishes in a row. Every waiter memorizes the rules slightly differently, and head office has to retrain each one by hand whenever the policy changes. Envoy is like replacing all those personal rulebooks with a single expediter window between the dining room and the kitchen: every order passes through it, and the window itself enforces "wait no more than two minutes, ask twice then stop, and quietly stop sending orders to the cook who's burnt the last three" — once, for every waiter, automatically. No waiter needs to know the rule exists, and head office changes the policy by updating the window, not retraining a hundred people.

🐢Your host for this topic: Timmy the Turtle — the reliability guardrail who taught reliability patterns and never trusts a service without a circuit breaker and a timeout. Envoy is where Timmy's rules stop being something a service promises to follow and start being something enforced in the network itself.

What Envoy is and the problem it solves

☺ Like you're 10: Instead of every app writing its own retry-and-circuit-breaker code, one proxy sitting next to every service does it identically for all of them, in every language at once.

Envoy was built at Lyft starting in 2015 by Matt Klein's infrastructure team, to solve a specific, unglamorous problem: Lyft ran services in half a dozen languages, and every one of them needed the same resilience behavior — retries, timeouts, circuit breaking, load balancing, TLS — reimplemented in whatever library that language happened to have. A Java service on Resilience4j and a Python service with a hand-rolled retry loop rarely behave identically under the same failure, and changing the retry policy meant coordinating a library bump and a redeploy across every repository that called the failing dependency. Lyft's answer was to move that logic out of application code entirely and into a proxy that sits next to every service, intercepts all of its inbound and outbound traffic, and applies the policy uniformly — no application code, no per-language library, no redeploy to change a timeout.

◆ Key idea

Envoy's own project description calls it a universal data plane API, and that phrase is precise. At L3/L4 it's a plain TCP/UDP proxy with a pluggable filter chain; layered on top, an HTTP Connection Manager filter adds HTTP/1.1, HTTP/2, gRPC, and HTTP/3 awareness, and a router filter within that picks an upstream cluster and applies the resilience policy — retries, timeouts, circuit breaking — to the request. Everything this page covers is filters in that chain, not a separate product bolted on.

Envoy by itself is not a service mesh — it's the proxy a mesh is built from. A mesh needs a control plane deciding what Envoy should do (which routes exist, which clusters are healthy, which certificates to present) and pushing that decision to every Envoy instance live; Envoy is the uniform enforcement point that control plane targets. That split — data plane versus control plane — is the architectural idea the rest of this page builds on.

Architecture: the proxy and the xDS discovery APIs

☺ Like you're 10: A single fast proxy sits next to every service, and instead of hand-editing its config file, a separate control plane streams it live updates it applies without dropping a single connection.

Envoy itself is a single binary written in C++, using non-blocking event-driven I/O, deliberately optimized for low tail latency and predictable behavior under load rather than raw throughput. It ships in three deployment shapes that all run the identical binary: as a sidecar — one Envoy process per application pod, with inbound and outbound traffic transparently redirected through it via iptables rules or a CNI, so the application never has to know it's there; as an edge/gateway proxy — a fleet of Envoy instances fronting a cluster, terminating TLS and routing north-south traffic the way NGINX or HAProxy traditionally did; and standalone, as the engine inside API gateways like Envoy Gateway, Contour, Emissary-ingress, and Gloo Edge.

What makes Envoy usable as a live mesh data plane rather than a static reverse proxy is xDS — a family of gRPC discovery services Envoy polls or streams from a control plane: LDS (Listener Discovery Service), RDS (routes), CDS (clusters), EDS (endpoints — the actual healthy IPs behind a cluster), and SDS (Secret Discovery Service, for TLS certificates and keys, so certs rotate without a restart). In practice these are almost always multiplexed over one gRPC stream as ADS (Aggregated Discovery Service), which also fixes an ordering problem: pushing a new route before its cluster exists would otherwise create a brief 503 window. A control plane — Istio's istiod, Consul's built-in one, or a custom one built on the go-control-plane library — computes this config from its own source of truth and streams it down; Envoy applies additions and changes without dropping in-flight connections, which is the property that makes reconfiguring a fleet of proxies a live operation instead of a rolling restart.

Downstream request Envoy sidecar or edge proxy Listener :10000 Network filter chain (L3/L4) HTTP Connection Manager router · retry · rate limit filters Cluster Manager outlier detection · circuit breakers pod-a — healthy pod-b — healthy pod-c — EJECTED cluster upstream traffic xDS control plane istiod · Consul · go-control-plane LDS · RDS · CDS · EDS · SDS one ADS gRPC stream pushes config, zero dropped conns Every stage above is configuration Envoy applies — none of it is application code.

The resilience toolkit, configured declaratively

☺ Like you're 10: The same four things reliability-patterns.html taught as app code — a way to notice a sick backend, a circuit breaker, retries, and timeouts — become a few dozen lines of YAML on a cluster or a route.

This is the direct payoff of putting a proxy in front of every service: the patterns from reliability patterns stop being a library each team adopts (or forgets to) and become fields on two Envoy resources — the Cluster (everything about a single upstream: how to find it, how to load-balance across it, when to eject a bad member of it) and the Route (everything about a single request path: which cluster it goes to, its timeout, its retry policy). Confirm exact field names and defaults against the current Envoy v3 API reference before shipping — Envoy's config surface has evolved across major API versions and individual fields do get deprecated.

Outlier detection — passive health checking

Outlier detection watches a cluster's real traffic — no separate health-check probe required — and ejects a member that looks unhealthy by its actual response pattern: a run of consecutive 5xx responses, a burst of consecutive gateway errors, or a success-rate outlier relative to its siblings using statistical outlier detection. An ejected host is pulled out of the load-balancing pool for a base ejection time that doubles on each subsequent ejection, then given another chance.

clusters:
- name: payments_service
  connect_timeout: 0.5s
  type: STRICT_DNS
  lb_policy: ROUND_ROBIN
  load_assignment:
    cluster_name: payments_service
    endpoints:
    - lb_endpoints:
      - endpoint: { address: { socket_address: { address: payments.internal, port_value: 8443 } } }
  outlier_detection:
    consecutive_5xx: 3            # eject after 3 consecutive 5xx from ONE host
    interval: 10s                 # how often the ejection sweep runs
    base_ejection_time: 30s       # doubles on repeated ejections of the same host
    max_ejection_percent: 50      # never eject more than half the cluster at once
    enforcing_consecutive_5xx: 100  # % of the time a qualifying host is actually ejected

max_ejection_percent is the single most important safety field in that block. If a shared dependency behind every member of a cluster fails at once — a database, a downstream all three replicas call — outlier detection watching only response codes would happily eject all of them, leaving nothing to route to. Capping the ejection percentage keeps at least some capacity in the pool even during a correlated failure; it doesn't fix the underlying dependency, but it stops the health check from making a bad outage total.

Circuit breaking — and why the name is misleading

⚠ Envoy's "circuit breaker" is not the pattern reliability-patterns.html taught

The classic circuit breaker — closed, open, half-open, with a cooldown and test requests to decide when to trust a dependency again — is what Envoy calls outlier detection, described above. What Envoy itself labels circuit_breakers is a different thing: a set of fixed resource ceilings per cluster — max concurrent connections, max pending requests, max concurrent requests, max concurrent retries — that reject new work immediately once hit, with no state machine and no automatic recovery test. It is a resource limiter, not a health-based breaker. Mixing the two up in an incident channel wastes minutes; know which one a given alert is actually about.

  circuit_breakers:
    thresholds:
    - priority: DEFAULT
      max_connections: 1024
      max_pending_requests: 1024
      max_requests: 1024
      max_retries: 3
      retry_budget:
        budget_percent: { value: 20.0 }   # retries capped at 20% of active request volume...
        min_retry_concurrency: 3          # ...but never below this floor

Retries — with a budget, not just a count

Reliability patterns already covered why naive retries can turn a blip into an outage: if every one of ten thousand requests-per-second hitting a struggling cluster retries three times, the cluster's effective load doesn't stay flat, it roughly quadruples right when it can least absorb it. A fixed num_retries per request doesn't fix that; it just moves the multiplier into config. A retry budget (the retry_budget field shown above) fixes the actual problem by expressing the retry ceiling as a percentage of current active request volume rather than a flat per-request count — so total retry traffic scales with real traffic instead of amplifying without bound as volume grows.

routes:
- match: { prefix: "/" }
  route:
    cluster: payments_service
    timeout: 2s                  # total budget for the whole route, all attempts included
    retry_policy:
      retry_on: "5xx,reset,connect-failure,refused-stream"
      num_retries: 3              # a per-request ceiling — the budget above caps it fleet-wide
      per_try_timeout: 0.5s       # each individual attempt's own deadline
      retry_back_off:
        base_interval: 0.1s
        max_interval: 2s
⚠ Envoy has no idea if your request is idempotent

The same caution from reliability patterns applies unchanged: retry_on: "5xx" retries a POST that already executed downstream just as eagerly as it retries a GET. Envoy cannot infer idempotency from an HTTP verb alone — a non-idempotent write behind a retried route needs a client-supplied idempotency key handled by the service itself, or it needs to be excluded from retry_on altogether.

Timeouts — three, not one

Envoy separates timeout concerns the way a careful reviewer would insist on by hand: connect_timeout on the cluster bounds how long establishing the TCP connection may take; route.timeout bounds the whole request, every retry attempt included; and per_try_timeout bounds a single attempt within that budget. Getting the relationship between the last two wrong is the most common timeout mistake — a per_try_timeout of 0.5s with three retries can consume up to 1.5s before the route-level 2s timeout ever fires, and if the caller's own client-side deadline is shorter than that, the caller gives up and disconnects while Envoy is still faithfully retrying into the void.

Why it became the default service-mesh data plane

☺ Like you're 10: Once every service already has this proxy standing next to it, that same connection can also carry a certificate, get encrypted, and report a trace span — a mesh is mostly just handing the proxy a few more jobs.

A service mesh needs exactly the shape Envoy already has: a proxy next to every service, capable of being reconfigured live, applying policy uniformly regardless of the application's language. Once that proxy exists, adding mutual TLS (via SDS-delivered certificates), consistent access logging, distributed tracing spans, and authorization checks is additive configuration on the same data plane rather than a second piece of infrastructure — see monitoring and observability for how that per-hop telemetry gets used. Istio, covered on its own page, is the clearest example: its sidecar container literally is Envoy, and istiod is the xDS control plane that translates Istio's own CRDs into the exact resources shown above — a DestinationRule's trafficPolicy.outlierDetection and connectionPool fields lower directly to Envoy's outlier_detection and circuit_breakers, and a VirtualService's retries block lowers to a route's retry_policy. Consul Connect and Gloo Mesh follow the same shape with their own control planes; Contour, Emissary-ingress, and the Envoy project's own Envoy Gateway use Envoy as an ingress data plane rather than a full mesh. Confirm current support status for any specific mesh product before committing to it — this is a fast-moving part of the ecosystem, and at least one major cloud provider's managed mesh offering has already been announced for deprecation since this course was written.

◆ Key idea

The alternative to a shared data plane is every mesh vendor reimplementing xDS-equivalent behavior from scratch — its own retry semantics, its own outlier detection, its own stats format. Standardizing on Envoy gave the ecosystem interoperable observability (the same admin stats, the same access log format) and interoperable resilience configuration underneath incompatible control-plane APIs on top. That's also why Kubernetes' Gateway API, and Cilium's optional L7 policy layer, both reach for Envoy rather than building a new proxy: the proxy layer had already been solved.

Day-to-day commands and operations

☺ Like you're 10: Almost nobody hand-edits Envoy's live config — instead you read it back through an admin endpoint, and change the source that generates it.

# standalone Envoy, started from a static bootstrap file
$ envoy -c bootstrap.yaml --service-cluster payments --service-node payments-1

# the admin interface — bound to 127.0.0.1:9901 by default, NEVER expose it publicly
$ curl -s localhost:9901/clusters | grep -E "health_flags|outlier"     # per-endpoint health state
$ curl -s localhost:9901/config_dump | jq '.configs[].dynamic_listeners'  # config actually applied
$ curl -s localhost:9901/stats/prometheus | grep retry                # retry/circuit-breaker counters
$ curl -s localhost:9901/ready                                        # readiness probe target
$ curl -X POST localhost:9901/logging?level=debug                     # bump log verbosity live

# Istio-managed Envoy sidecars: istioctl wraps the same admin data per pod
$ istioctl proxy-config cluster  checkout-7d6c9-abcde -n prod
$ istioctl proxy-config route    checkout-7d6c9-abcde -n prod
$ istioctl proxy-status                       # xDS sync state across the whole mesh: ACK / NACK / STALE

Standalone Envoy's classic reconfiguration mechanism — a hot restart, where a new worker process attaches to the old one's listening sockets over a shared-memory handoff so no connection drops during a binary upgrade — matters mostly for edge/gateway deployments managing their own lifecycle. Inside a mesh, config changes normally arrive over xDS with no restart of any kind; the hot-restart machinery is largely invisible unless you're operating standalone Envoy directly.

Gotchas and failure modes

☺ Like you're 10: Most Envoy surprises come from a safety knob nobody tuned, a config push that silently didn't apply, or forgetting that a proxy in front of every pod isn't free.

Alternatives and when to choose it

☺ Like you're 10: Other tools also route traffic and can retry or time out a call — they just trade away Envoy's live, uniform, cross-language configuration in different ways.

OptionModelBest whenCosts you
EnvoyC++ L3/L4/L7 proxy, sidecar or edge, live-reconfigured via xDSPolyglot fleet needing uniform resilience policy and observability, or the data plane under a service meshAn extra process per pod (memory, CPU, one hop of latency); config is normally generated by a control plane, not hand-written
Linkerd2-proxyRust "micro-proxy" purpose-built for Linkerd, minimal feature surface by designKubernetes-only, mTLS and basic resilience are the goal, and the smallest possible sidecar footprint matters more than Envoy's breadthNot a general-purpose proxy — no standalone edge/gateway use, far fewer filters than Envoy's ecosystem
NGINX / HAProxyMature reverse proxies, config reload rather than a live discovery API by defaultA straightforward edge load balancer or reverse proxy, team already fluent in the config language, no per-pod sidecar neededNo native xDS-style live reconfiguration or per-language-agnostic sidecar deployment model; retry/circuit-breaking config is less granular
Application-level libraries (Resilience4j, Polly)Resilience logic lives inside the service's own process and languageA small, single-language codebase where an extra network hop and process per pod isn't worth itReimplemented per language, drifts across services, requires a redeploy to change a timeout or retry policy
Cloud-managed load balancer (ALB/NLB)Fully managed L4/L7 balancing, no proxy you operateSimple ingress in front of a service with no need for mesh-grade retry budgets or outlier detectionCoarser resilience controls than Envoy's filter chain; no sidecar model for east-west traffic between services

The practical rule: reach for application-level libraries when the system is small and single-language and an extra hop isn't worth it; reach for a lighter proxy like Linkerd2-proxy when Kubernetes-only mTLS and basic resilience is genuinely all that's needed; reach for Envoy — usually via a mesh control plane rather than hand-written config — once the fleet is polyglot, the resilience policy needs to be uniform and centrally changeable, or a mesh's other benefits (mTLS, tracing, authorization) are wanted on the same data plane. See the SRE toolchain for where Envoy sits alongside the rest of the observability and delivery stack, and Kubernetes reliability patterns for how the sidecar model interacts with pod scheduling and probes.

🎬 At the Reliability Watch
🐢

Timmy the Turtle: Checkout's retries just tripled traffic to payments the moment it started failing. Where's the budget?

🦫

Benny the Beaver: num_retries: 3, applied to every one of the ten thousand requests a second we were already sending. I didn't check what that does under load.

🦊

Foxy: So the struggling downstream gets roughly four times the requests right when it can least handle it. That's the retry storm from the reliability-patterns page again, just happening in the proxy instead of app code.

🐢

Timmy the Turtle: Set a retry_budget instead — cap it at 20% of active requests, not a flat multiplier per call. And check max_ejection_percent before you ship this: if payments' three backends all trip the same 5xx check at once, we don't want outlier detection ejecting all three together.

🦥

Sol the Sloth: While you two argue, I already checked the panic threshold — it kicks in under 50% healthy and sends to everyone anyway, so you won't accidentally black-hole the whole cluster. Don't treat that valve as your actual design, though. It's a last resort, not a plan.

Going further

☺ Like you're 10: This page is the proxy; the control plane that usually drives it in production is a separate page.

The canonical source is the Envoy documentation and v3 API reference at envoyproxy.io/docs — confirm current field names, defaults, and API version against it, since Envoy's config surface iterates faster than a stable, rarely-changing OSS API — plus the source at github.com/envoyproxy/envoy and the CNCF project page at cncf.io/projects/envoy. Pair this page with reliability patterns for the application-level version of everything covered here, Istio for the control plane that drives most production Envoy fleets, chaos engineering for deliberately testing whether an outlier-detection or retry-budget configuration actually holds under a real fault, and the glossary for any term above that didn't stick on first read.

✓ Checkpoint

1. What does Envoy call the classic closed/open/half-open circuit breaker pattern, and what does it call the fixed resource-ceiling mechanism most people expect that name to mean? 2. Why does a flat num_retries per request not solve the retry-storm problem, and what field does? 3. Name the five xDS discovery services and what each one delivers. 4. What is the panic threshold, and what real failure mode does it protect against? 5. Why is Envoy, by itself, not a service mesh?

Check your answers
  1. The closed/open/half-open pattern is Envoy's outlier detection — passive, health-based ejection of a misbehaving upstream. What Envoy itself labels circuit_breakers is a set of fixed resource ceilings (max connections, pending requests, concurrent requests, concurrent retries) with no state machine and no automatic recovery test — a resource limiter, not a breaker in the classic sense.
  2. A fixed num_retries is applied per request regardless of how much total traffic a cluster is already receiving, so as request volume grows, retry-amplified load grows right alongside it — potentially quadrupling load onto a cluster exactly when it's already struggling. A retry_budget (expressed as a percentage of current active request volume, with a concurrency floor) caps total retry traffic proportionally to real traffic instead.
  3. LDS (Listener Discovery Service — listeners), RDS (Route Discovery Service — routes), CDS (Cluster Discovery Service — clusters/upstreams), EDS (Endpoint Discovery Service — the actual healthy endpoint IPs behind a cluster), and SDS (Secret Discovery Service — TLS certificates and keys). In practice these are usually multiplexed over one gRPC stream as ADS.
  4. The panic threshold is the point (by default, fewer than 50% of relevant hosts healthy) at which Envoy's load balancer stops honoring health status and routes to every host, healthy or not. It protects against a correlated failure — many hosts in a cluster failing the same health check at once — turning outlier detection into an accidental black hole of the entire cluster instead of a targeted ejection of the bad members.
  5. Envoy is the data plane — the proxy that enforces policy — but a mesh also needs a control plane deciding what that policy should be (which routes and clusters exist, which certificates to present, which hosts are healthy) and pushing it live to every Envoy instance. Envoy alone has no opinion about mesh-wide identity, routing intent, or authorization policy; that's supplied by a control plane such as Istio's istiod.