Envoy Proxy Rate Limiting: Global and Local Strategies
Envoy Proxy Rate Limiting: Global and Local Strategies
Rate limiting is one of those things that seems simple until you actually need it at scale. A single service can apply limits locally, but the moment you have multiple Envoy instances in a mesh or as edge proxies, "per-service" limits stop being meaningful — each instance only sees a fraction of the total traffic.
This guide covers both local rate limiting (fast, no dependencies) and global rate limiting (consistent across all instances, backed by a shared state store), with working Envoy configurations for each.
Local Rate Limiting: Token Bucket per Filter Chain
Local rate limiting runs entirely within the Envoy process. No external calls, no shared state. It's the fastest option and works well for per-connection or per-listener limits.
# envoy.yaml — Local rate limit via HTTP filter
static_resources:
listeners:
- name: main_listener
address:
socket_address:
address: 0.0.0.0
port_value: 8080
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
http_filters:
# Local rate limit filter — applied before routing
- name: envoy.filters.http.local_ratelimit
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
stat_prefix: http_local_rate_limiter
token_bucket:
max_tokens: 1000
tokens_per_fill: 1000
fill_interval: 1s
filter_enabled:
runtime_key: local_rate_limit_enabled
default_value:
numerator: 100
denominator: HUNDRED
filter_enforced:
runtime_key: local_rate_limit_enforced
default_value:
numerator: 100
denominator: HUNDRED
response_headers_to_add:
- append: false
header:
key: x-local-rate-limit
value: 'true'
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
The token_bucket here allows 1000 requests per second per Envoy instance. Clients that exceed it get a 429 Too Many Requests response immediately — no upstream service is touched.
Per-Route Local Limits
You can override the global limit on specific routes:
virtual_hosts:
- name: app_service
domains: ["*"]
routes:
- match:
prefix: /api/public
route:
cluster: public_api_cluster
typed_per_filter_config:
envoy.filters.http.local_ratelimit:
"@type": type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
stat_prefix: public_api_local_ratelimit
token_bucket:
max_tokens: 10000 # Higher limit for public endpoints
tokens_per_fill: 10000
fill_interval: 1s
- match:
prefix: /api/admin
route:
cluster: admin_api_cluster
typed_per_filter_config:
envoy.filters.http.local_ratelimit:
"@type": type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
stat_prefix: admin_api_local_ratelimit
token_bucket:
max_tokens: 50 # Strict limit on admin endpoints
tokens_per_fill: 50
fill_interval: 1s
Global Rate Limiting: Redis-Backed External Service
For consistent limits across a fleet of Envoy proxies, you need a shared state store. The standard approach is the Envoy rate limit service backed by Redis.
Deploy the Rate Limit Service
# ratelimit-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: ratelimit
namespace: envoy-system
spec:
replicas: 2
selector:
matchLabels:
app: ratelimit
template:
metadata:
labels:
app: ratelimit
spec:
containers:
- name: ratelimit
image: envoyproxy/ratelimit:master
command: ["/bin/ratelimit"]
env:
- name: LOG_LEVEL
value: warn
- name: REDIS_SOCKET_TYPE
value: tcp
- name: REDIS_URL
value: redis:6379
- name: USE_STATSD
value: "false"
- name: RUNTIME_ROOT
value: /data
- name: RUNTIME_SUBDIRECTORY
value: ratelimit
- name: RUNTIME_WATCH_ROOT
value: "false"
- name: RUNTIME_IGNOREDOTFILES
value: "true"
ports:
- containerPort: 8080
name: http
- containerPort: 8081
name: grpc
volumeMounts:
- name: config
mountPath: /data/ratelimit/config
volumes:
- name: config
configMap:
name: ratelimit-config
---
apiVersion: v1
kind: Service
metadata:
name: ratelimit
namespace: envoy-system
spec:
selector:
app: ratelimit
ports:
- name: grpc
port: 8081
targetPort: 8081
Rate Limit Configuration
# ratelimit-config.yaml (ConfigMap data)
apiVersion: v1
kind: ConfigMap
metadata:
name: ratelimit-config
namespace: envoy-system
data:
config.yaml: |
domain: api_limits
descriptors:
# Global limit: 10,000 req/min across all instances
- key: generic_key
value: default
rate_limit:
unit: MINUTE
requests_per_unit: 10000
# Per-user limit: 100 req/min per user ID
- key: user_id
rate_limit:
unit: MINUTE
requests_per_unit: 100
# Per-IP limit: 50 req/min for auth endpoints
- key: remote_address
descriptors:
- key: path
value: /auth/login
rate_limit:
unit: MINUTE
requests_per_unit: 10
# Tiered: premium users get 1000/min, free gets 100/min
- key: plan_type
value: premium
rate_limit:
unit: MINUTE
requests_per_unit: 1000
- key: plan_type
value: free
rate_limit:
unit: MINUTE
requests_per_unit: 100
Wire Envoy to the Global Rate Limit Service
# Envoy config — global rate limit integration
http_filters:
- name: envoy.filters.http.ratelimit
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit
domain: api_limits
request_type: external
stage: 0
rate_limited_as_resource_exhausted: true
rate_limit_service:
grpc_service:
envoy_grpc:
cluster_name: rate_limit_cluster
timeout: 0.25s # Fail open if rate limit service is slow
transport_api_version: V3
failure_mode_deny: false # fail open — don't block traffic if RLS is down
# Add the rate limit cluster
clusters:
- name: rate_limit_cluster
type: STRICT_DNS
connect_timeout: 1s
lb_policy: ROUND_ROBIN
http2_protocol_options: {}
load_assignment:
cluster_name: rate_limit_cluster
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: ratelimit.envoy-system.svc.cluster.local
port_value: 8081
Send Descriptors from Route Config
virtual_hosts:
- name: api
domains: ["api.example.com"]
routes:
- match:
prefix: /
route:
cluster: backend_service
rate_limits:
- stage: 0
actions:
# Always send generic_key for global limit
- generic_key:
descriptor_value: default
# Send user ID from header for per-user limits
- request_headers:
header_name: x-user-id
descriptor_key: user_id
skip_if_absent: true
# Send client IP for IP-based limits
- remote_address: {}
Combining Local and Global Limits
Use both together for defense in depth:
| Layer | Mechanism | Purpose |
|---|---|---|
| Local (per instance) | Token bucket | Fast protection against burst traffic |
| Global (shared Redis) | External rate limit service | Consistent cross-instance limits |
The local limit fires first — no network call needed. The global limit enforces aggregate quotas. A request only hits the upstream service if it passes both.
Testing Your Rate Limits
# Install hey for load testing
go install github.com/rakyll/hey@latest
# Test local rate limit (1000 req/sec configured)
hey -n 5000 -c 100 -q 2000 http://localhost:8080/api/test
# Watch Envoy rate limit stats
curl -s localhost:9901/stats | grep ratelimit
# Expected stats output:
# http.ingress_http.ratelimit.ok: 1000
# http.ingress_http.ratelimit.over_limit: 4000
# http.ingress_http.ratelimit.error: 0
Rate Limit Response Headers
Add informational headers so clients can implement backoff:
response_headers_to_add:
- header:
key: x-ratelimit-limit
value: "%DYNAMIC_METADATA(envoy.ratelimit:limit)%"
keep_empty_value: false
- header:
key: x-ratelimit-remaining
value: "%DYNAMIC_METADATA(envoy.ratelimit:remaining)%"
keep_empty_value: false
- header:
key: x-ratelimit-reset
value: "%DYNAMIC_METADATA(envoy.ratelimit:reset)%"
keep_empty_value: false
- header:
key: retry-after
value: "1"
response_code_details: rate_limit_response_code
These headers follow the IETF draft standard for rate limit headers, which well-behaved HTTP clients can use to implement exponential backoff automatically.
Key Takeaways
- Local rate limiting is zero-latency and the right first line of defense for burst protection
- Global rate limiting requires a rate limit service and Redis but provides consistent cross-fleet limits
failure_mode_deny: false(fail open) is the safer default — a Redis outage shouldn't take down your API- Set the rate limit service timeout low (250ms) to prevent slow Redis from adding latency to every request
- Use descriptors to implement tiered limits per user tier, IP, or endpoint — the rate limit service config is just YAML, no code changes needed
Was this article helpful?
Platform Engineer
Terraform enthusiast, platform builder, DRY advocate. I believe infrastructure should be versioned, reviewed, and deployed like any other code. GitOps or bust.
Related Articles
Envoy Proxy for Microservices: Edge and Sidecar Patterns
Deploy Envoy as an edge proxy and sidecar for microservices — listeners, clusters, routes, circuit breaking, retries, and observability features.
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.
Envoy Proxy: Architecture, xDS Configuration, and Getting Started
An introduction to Envoy Proxy's architecture — listeners, clusters, filters, and the xDS dynamic configuration API. Covers static configuration for standalone use and how Envoy fits into service meshes like Istio.
Fix Envoy Proxy 'upstream connect error or disconnect/reset before headers'
Resolve Envoy's 'upstream connect error or disconnect/reset before headers' error caused by service mesh misconfiguration, timeout issues, or unreachable backends.
Envoy Observability: Distributed Tracing and Metrics
Configure Envoy distributed tracing with Jaeger, expose Prometheus metrics, and build end-to-end observability for your service mesh.
Istio Service Mesh: Installation, Traffic Management, and mTLS
A practical guide to getting started with Istio — installing on Kubernetes, enabling automatic mTLS, configuring VirtualServices for traffic management, and understanding the sidecar injection model.