Envoy Global Rate Limiting: Architecture and Practical Trade-offs
Envoy's global rate limiting uses a separate gRPC Rate Limit Service (RLS) to enforce quotas across proxy fleets. This post walks through the descriptor model, a minimal Envoy+RLS config, and the latency/failure trade-offs you'll face in production.
04 Jun 2026, 01:44 UTC

The problem: quotas across a fleet
When you run dozens of Envoy sidecars or edge proxies, per-instance rate limiting isn't enough. A single client can hammer different instances and bypass local counters. You need a single source of truth for quotas — enter the Rate Limit Service (RLS), a separate gRPC service that Envoy calls on every request.
How the descriptor model works
Envoy doesn't send "user 123, path /api" as a raw string. It builds a descriptor — a list of (key, value) tuples — that the RLS evaluates against its rule set. A typical descriptor might look like:
descriptors:
- entries:
- key: "remote_address"
value: "10.0.0.5"
- key: "request_path"
value: "/api/v1/payments"
- key: "generic_key"
value: "user_123"
The RLS (often the reference implementation in envoyproxy/rate-limit-service) matches these entries against a YAML/JSON config that defines limits like "100 requests per minute per user on /payments". Because the descriptor is arbitrary, you can rate-limit by API key, Kubernetes namespace, or any header you propagate.
Worked example: wiring Envoy to the RLS
Below is a minimal envoy.yaml snippet that enables the HTTP rate limit filter and points it at a local RLS instance. Run this on a test box; the RLS container can be started with docker run -p 8081:8081 -p 8082:8082 envoyproxy/rate-limit-service:latest.
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
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match: { prefix: "/" }
route: { cluster: service_cluster }
http_filters:
- name: envoy.filters.http.ratelimit
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit
domain: envoy
failure_mode_deny: false # fail-open
rate_limit_service:
grpc_service:
envoy_grpc:
cluster_name: rate_limit_cluster
transport_api_version: V3
- name: envoy.filters.http.router
clusters:
- name: service_cluster
type: STATIC
load_assignment:
cluster_name: service_cluster
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address: { address: host.docker.internal, port_value: 8080 }
- name: rate_limit_cluster
type: STATIC
load_assignment:
cluster_name: rate_limit_cluster
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address: { address: host.docker.internal, port_value: 8081 }
With this config, every inbound request triggers an async gRPC call to rate_limit_cluster. If the RLS replies OVER_LIMIT, Envoy returns 429 Too Many Requests with a Retry-After header. The failure_mode_deny: false setting means a downed RLS results in allowing the request (fail-open); flip to true for fail-closed.
Latency, failure modes, and the thundering herd
The mandatory gRPC round-trip adds 1–5 ms per request on a local network — acceptable for most APIs, but measurable on hot paths. Two operational knobs matter:
- Timeouts: Set
timeouton thegrpc_service(default 20 ms). Too short and you get false429s; too long and you add tail latency. - Fail-open vs fail-closed:
failure_mode_denychooses the behavior when the RLS is unreachable. Fail-open preserves availability but lets bursts through; fail-closed protects quotas but can turn an RLS outage into a full outage.
Under heavy load, the RLS itself can become a bottleneck. The reference service uses Redis for counter storage; if Redis saturates, every Envoy instance piles onto the same failing backend — the classic thundering herd. Mitigations include:
- Running multiple RLS replicas behind a load balancer.
- Enabling local rate limiting (
envoy.filters.http.local_ratelimit) as a first line of defense for known abusive patterns. - Configuring
rate_limit_service.rate_limit_headersto propagateX-RateLimit-LimitandX-RateLimit-Remainingso clients can back off early.
Verify it works
- Start the RLS and Envoy with the config above.
- Send a burst:
hey -n 200 -c 10 http://localhost:10000/api/v1/payments. - Confirm responses shift from
200to429once the descriptor limit is hit. - Check Envoy stats:
curl localhost:9901/stats | grep ratelimit— look forratelimit.over_limitincrements.
If you see 429s and the counters climb, the integration is functional. Next step: tune the RLS rule config (usually config.yaml mounted into the RLS container) to match your actual quota policy.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.