Envoy Proxy: Architecture, xDS Configuration, and Getting Started
Why Envoy Exists
HAProxy and Nginx were designed for traditional load balancing. Envoy was designed for service-to-service communication in distributed systems. The difference is architectural:
- Envoy is L3/L4/L7-aware — it understands HTTP/2, gRPC, and Thrift natively
- Envoy has first-class observability — every request generates stats, traces, and logs by default
- Envoy is dynamically configurable — its xDS API lets a control plane push config changes without restarts
- Envoy runs as a sidecar — one Envoy per service instance, forming a service mesh
Understanding Envoy's building blocks makes both standalone use and service mesh deployments (Istio, AWS App Mesh) comprehensible.
Core Concepts
Envoy's configuration model has four key primitives:
| Primitive | Description |
|---|---|
| Listener | Binds to an IP:port, receives incoming connections |
| Filter Chain | Pipeline of network/HTTP filters applied to traffic |
| Route | Maps incoming requests to clusters |
| Cluster | Upstream service pool with load balancing and health checks |
Traffic flows: Listener → Filter Chain → Route → Cluster → Upstream
Installation
# Add Envoy apt repo (Ubuntu/Debian)
sudo apt install -y debian-archive-keyring apt-transport-https lsb-release ca-certificates gnupg
curl -sL 'https://deb.dl.getenvoy.io/public/gpg.8115BA8E629CC074.key' | sudo gpg --dearmor -o /usr/share/keyrings/getenvoy-keyring.gpg
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/getenvoy-keyring.gpg] https://deb.dl.getenvoy.io/public/deb/ubuntu $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/getenvoy.list
sudo apt update && sudo apt install envoy -y
# Or run via Docker
docker run --rm envoyproxy/envoy:v1.32-latest --version
# Verify
envoy --version
Static Configuration
Envoy configuration is YAML (or JSON). A complete static config:
envoy.yaml:
static_resources:
listeners:
- name: listener_0
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
access_log:
- name: envoy.access_loggers.stdout
typed_config:
"@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match:
prefix: "/"
route:
cluster: service_backend
clusters:
- name: service_backend
connect_timeout: 5s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: service_backend
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: 127.0.0.1
port_value: 3000
admin:
address:
socket_address:
address: 0.0.0.0
port_value: 9901
Run Envoy:
envoy -c envoy.yaml
# Or with Docker:
docker run --rm -v $(pwd)/envoy.yaml:/etc/envoy/envoy.yaml \
-p 10000:10000 -p 9901:9901 \
envoyproxy/envoy:v1.32-latest
HTTP Routing with Multiple Clusters
Route different paths to different backends:
route_config:
virtual_hosts:
- name: main
domains: ["*"]
routes:
# Route /api/* to API cluster
- match:
prefix: "/api/"
route:
cluster: api_cluster
timeout: 30s
# Route /static/* to static asset cluster
- match:
prefix: "/static/"
route:
cluster: static_cluster
# Default
- match:
prefix: "/"
route:
cluster: web_cluster
Load Balancing Policies
clusters:
- name: web_cluster
lb_policy: LEAST_REQUEST # Route to server with fewest active requests
load_assignment:
cluster_name: web_cluster
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address: { address: 10.0.1.10, port_value: 8080 }
load_balancing_weight: 1
- endpoint:
address:
socket_address: { address: 10.0.1.11, port_value: 8080 }
load_balancing_weight: 2 # gets 2x traffic
Available policies: ROUND_ROBIN, LEAST_REQUEST, RING_HASH, RANDOM, MAGLEV
Health Checking
clusters:
- name: web_cluster
health_checks:
- timeout: 3s
interval: 10s
unhealthy_threshold: 3 # fail 3x to mark unhealthy
healthy_threshold: 2 # pass 2x to mark healthy
http_health_check:
path: /health
expected_statuses:
- start: 200
end: 299
Observability: Stats and Tracing
Envoy exposes rich metrics at the admin endpoint:
# Admin dashboard
curl http://localhost:9901/
# All stats
curl http://localhost:9901/stats
# Filter stats
curl http://localhost:9901/stats?filter=cluster.web_cluster
# Prometheus format
curl http://localhost:9901/stats/prometheus
# Ready check
curl http://localhost:9901/ready
Enable Zipkin tracing:
static_resources:
...
tracing:
http:
name: envoy.tracers.zipkin
typed_config:
"@type": type.googleapis.com/envoy.config.trace.v3.ZipkinConfig
collector_cluster: zipkin
collector_endpoint: "/api/v2/spans"
collector_endpoint_version: HTTP_JSON
Circuit Breaking
Prevent cascading failures by limiting pending requests and retries:
clusters:
- name: web_cluster
circuit_breakers:
thresholds:
- priority: DEFAULT
max_connections: 100
max_pending_requests: 100
max_requests: 1000
max_retries: 3
When thresholds are exceeded, Envoy returns 503 immediately rather than queuing indefinitely.
The xDS Dynamic API
In production (and in Istio), Envoy configuration is pushed dynamically via the xDS API rather than static YAML. A control plane sends:
- LDS (Listener Discovery Service) — listener config
- RDS (Route Discovery Service) — routing rules
- CDS (Cluster Discovery Service) — cluster definitions
- EDS (Endpoint Discovery Service) — individual endpoint IPs
dynamic_resources:
lds_config:
resource_api_version: V3
api_config_source:
api_type: GRPC
transport_api_version: V3
grpc_services:
- envoy_grpc:
cluster_name: xds_cluster
cds_config:
resource_api_version: V3
api_config_source:
api_type: GRPC
transport_api_version: V3
grpc_services:
- envoy_grpc:
cluster_name: xds_cluster
static_resources:
clusters:
- name: xds_cluster
type: STATIC
load_assignment:
cluster_name: xds_cluster
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: control-plane.example.com
port_value: 18000
This is how Istio works: Istiod is the xDS control plane, every pod's Envoy sidecar receives routing rules dynamically.
Envoy as a Sidecar vs. Edge Proxy
| Role | Description |
|---|---|
| Edge proxy | One Envoy at the cluster ingress (like an Nginx/HAProxy replacement) |
| Sidecar | One Envoy per pod/service, intercepting all in/outbound traffic |
| Service mesh | All sidecars + a control plane = Istio, Consul Connect, AWS App Mesh |
For standalone edge proxy use: use static config as shown above. For Kubernetes service mesh: use Istio or Consul (they manage Envoy configuration automatically).
Was this article helpful?
Network & Traffic Engineer
Packets don't lie. I design and troubleshoot the network layer that everything else depends on — Nginx, Envoy, HAProxy, DNS, CDNs, and everything in between. If it touches a socket, it's my problem.
Related Articles
Envoy Traffic Management: Retries, Timeouts, Canary Deployments, and Rate Limiting
Advanced Envoy traffic management — configuring retries with exponential backoff, per-request timeouts, weighted canary routing, global rate limiting, and fault injection for resilience testing.
HAProxy Configuration Guide: TCP/HTTP Load Balancing, Health Checks, and SSL
A comprehensive HAProxy configuration guide covering frontends, backends, balance algorithms, active health checks, SSL termination, ACLs, rate limiting, and the runtime socket API.
HAProxy + Keepalived: Production HA Load Balancer Setup
Step-by-step guide to building a highly available load balancer pair with HAProxy and Keepalived — covering the full stack from VIP configuration to health checks, stats, and SSL termination.
Keepalived and VRRP: Building High-Availability Failover for Linux Services
How to configure Keepalived for automatic failover using VRRP — setting up master/backup pairs, virtual IPs, health-check scripts, and combining it with HAProxy or Nginx for zero-downtime load balancer HA.
Apache mod_proxy: Reverse Proxy, Load Balancing, and WebSocket Support
How to use Apache httpd as a reverse proxy with mod_proxy — proxying to backend services, load balancing across multiple upstreams, WebSocket proxying, and health check configuration.
HAProxy Advanced: SSL Termination, SNI Routing, and mTLS
Advanced HAProxy SSL configuration — multi-domain SNI routing from a single frontend, mutual TLS (mTLS) for service-to-service authentication, certificate management, and OCSP stapling.
More in Nginx
View all →Apache httpd: Virtual Hosts, SSL/TLS, and URL Rewriting in Production
How to configure Apache httpd for production use — name-based virtual hosts, SSL/TLS with Let's Encrypt, HTTP to HTTPS redirects, mod_rewrite rules, and performance tuning with MPM.
Fix Nginx 502 Bad Gateway Behind a Reverse Proxy
Diagnose and fix Nginx 502 Bad Gateway errors when proxying to upstream backends — check sockets, timeouts, and upstream health.
Fix Nginx 'Too Many Open Files' Error
Resolve Nginx 'Too many open files' errors by increasing worker_rlimit_nofile, system ulimits, and kernel file descriptor limits.
Nginx Load Balancing: Round Robin, Least Conn, and IP Hash
Configure Nginx upstream load balancing with round robin, least connections, and IP hash strategies including health checks and failover.