Envoy Proxy: Rate Limiting and Traffic Control
Quick answer
Protect APIs and services with Envoy's rate limiting filters — local (per-instance token bucket) and global (coordinated via an external rate limit service). Includes Kubernetes integration via Envoy Gateway.
- How Envoy Configures Itself
- Local Rate Limiting
- Global Rate Limiting
- Rate Limit Response Headers
- Rate Limiting in Kubernetes: Envoy Gateway
advanced · 60 min
Before you begin
- Solid understanding of HTTP, reverse proxies, and Kubernetes networking
- Envoy or Istio/Envoy Gateway running, or willingness to deploy one
- Familiarity with YAML-based Kubernetes configuration
Envoy Proxy: Rate Limiting and Traffic Control
Envoy is a high-performance Layer 7 proxy written in C++ and used as the data plane in Istio, Consul Connect, AWS App Mesh, and directly as a standalone edge proxy or sidecar. It supports two fundamentally different approaches to rate limiting:
- Local rate limiting — enforced independently by each Envoy instance, no coordination. Simple and low-latency.
- Global rate limiting — Envoy sends a gRPC request to an external rate limit service for every request. Accurate across a fleet of instances.
How Envoy Configures Itself
Envoy uses a bootstrap config (JSON or YAML) that defines:
- Listeners — bind to a port, accept connections
- Filter chains — process the connection (e.g., HTTP connection manager)
- HTTP filters — process each HTTP request (router, rate limiter, JWT auth, etc.)
- Clusters — upstream backend pools
- Routes — match request to cluster
Configuration can be static (in the bootstrap file) or dynamic (via xDS APIs — ADS, EDS, CDS, LDS, RDS — served by a control plane like Istio Pilot).
Local Rate Limiting
Local rate limiting uses a token bucket — each Envoy instance maintains its own bucket independent of other instances. A rate of 100 requests/minute with 3 replicas means 300 requests/minute cluster-wide.
Filter configuration
1# Static Envoy config — apply local_ratelimit to an HTTP listener
2http_filters:
3 - name: envoy.filters.http.local_ratelimit
4 typed_config:
5 "@type": type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
6 stat_prefix: http_local_rate_limiter
7 token_bucket:
8 max_tokens: 100 # Bucket capacity
9 tokens_per_fill: 100 # Tokens added per fill interval
10 fill_interval: 60s # Refill every 60 seconds (100 req/min)
11 filter_enabled:
12 runtime_key: local_rate_limit_enabled
13 default_value:
14 numerator: 100
15 denominator: HUNDRED # Always enabled
16 filter_enforced:
17 runtime_key: local_rate_limit_enforced
18 default_value:
19 numerator: 100
20 denominator: HUNDRED # Always enforced (not just sampled)
21 response_headers_to_add:
22 - append: false
23 header:
24 key: x-local-rate-limit
25 value: "true"When the bucket is exhausted, Envoy returns 429 Too Many Requests.
Per-route override
Apply different limits to different routes:
1routes:
2 - match:
3 prefix: "/api/public"
4 route:
5 cluster: api_cluster
6 typed_per_filter_config:
7 envoy.filters.http.local_ratelimit:
8 "@type": type.googleapis.com/udpa.type.v1.TypedStruct
9 type_url: type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
10 value:
11 stat_prefix: api_public
12 token_bucket:
13 max_tokens: 1000
14 tokens_per_fill: 1000
15 fill_interval: 60s
16
17 - match:
18 prefix: "/api/admin"
19 route:
20 cluster: api_cluster
21 typed_per_filter_config:
22 envoy.filters.http.local_ratelimit:
23 "@type": type.googleapis.com/udpa.type.v1.TypedStruct
24 type_url: type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
25 value:
26 stat_prefix: api_admin
27 token_bucket:
28 max_tokens: 10
29 tokens_per_fill: 10
30 fill_interval: 60sGlobal Rate Limiting
Global rate limiting sends a gRPC ShouldRateLimit request to an external service before each proxied request. The service maintains a shared counter — all Envoy instances coordinate through it.
The rate limit service
envoy/ratelimit is the reference implementation. It reads a Redis backend and answers Envoy's gRPC requests.
Deploy it alongside a Redis instance:
1# rate-limit-service.yaml
2apiVersion: apps/v1
3kind: Deployment
4metadata:
5 name: ratelimit
6 namespace: envoy-system
7spec:
8 replicas: 2
9 selector:
10 matchLabels:
11 app: ratelimit
12 template:
13 metadata:
14 labels:
15 app: ratelimit
16 spec:
17 containers:
18 - name: ratelimit
19 image: envoyproxy/ratelimit:master
20 env:
21 - name: LOG_LEVEL
22 value: "warn"
23 - name: REDIS_SOCKET_TYPE
24 value: "tcp"
25 - name: REDIS_URL
26 value: "redis:6379"
27 - name: USE_STATSD
28 value: "false"
29 - name: RUNTIME_ROOT
30 value: /data
31 - name: RUNTIME_SUBDIRECTORY
32 value: ratelimit
33 - name: RUNTIME_WATCH_ROOT
34 value: "false"
35 - name: RUNTIME_IGNOREDOTFILES
36 value: "true"
37 volumeMounts:
38 - name: config
39 mountPath: /data/ratelimit/config
40 volumes:
41 - name: config
42 configMap:
43 name: ratelimit-config
44---
45apiVersion: v1
46kind: Service
47metadata:
48 name: ratelimit
49 namespace: envoy-system
50spec:
51 selector:
52 app: ratelimit
53 ports:
54 - name: grpc
55 port: 8081
56 targetPort: 8081Rate limit service config
1# ConfigMap containing the rate limit rules
2apiVersion: v1
3kind: ConfigMap
4metadata:
5 name: ratelimit-config
6 namespace: envoy-system
7data:
8 config.yaml: |
9 domain: my-api
10 descriptors:
11 # 100 requests per minute per remote address
12 - key: remote_address
13 rate_limit:
14 unit: MINUTE
15 requests_per_unit: 100
16
17 # 10 requests per minute for the /api/admin path
18 - key: path
19 value: /api/admin
20 rate_limit:
21 unit: MINUTE
22 requests_per_unit: 10
23
24 # 50 requests per minute per user ID header
25 - key: user_id
26 rate_limit:
27 unit: MINUTE
28 requests_per_unit: 50Envoy filter config for global rate limiting
1http_filters:
2 - name: envoy.filters.http.ratelimit
3 typed_config:
4 "@type": type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit
5 domain: my-api # Must match the domain in rate limit service config
6 request_type: external
7 failure_mode_deny: false # true = deny requests if rate limit service is unreachable
8 rate_limit_service:
9 grpc_service:
10 envoy_grpc:
11 cluster_name: rate_limit_service
12 timeout: 0.25s # Don't add more than 250ms to request latency
13 transport_api_version: V3Rate limit descriptors and actions
Descriptors tell the rate limit service how to categorize each request. Actions extract values from the request to build the descriptor.
1# In the route config
2rate_limits:
3 - actions:
4 - remote_address: {} # Limit by client IP
5
6 - actions:
7 - request_headers:
8 header_name: ":path"
9 descriptor_key: "path" # Match 'path' key in rate limit config
10
11 - actions:
12 - request_headers:
13 header_name: "x-user-id"
14 descriptor_key: "user_id"
15 skip_if_absent: true # Skip this descriptor if header missing
16
17 - actions:
18 - generic_key:
19 value: "global" # Fixed key for a global limitMultiple actions in a single rate_limits entry are combined into one compound descriptor (all must match). Multiple top-level rate_limits entries are OR'd — any matching limit applies.
Rate Limit Response Headers
Envoy can add standard rate limit headers so clients know their current quota:
1typed_config:
2 "@type": type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit
3 response_headers_to_add:
4 - header:
5 key: x-ratelimit-limit
6 value: "%DYNAMIC_METADATA(envoy.filters.http.ratelimit:quota)%"
7 - header:
8 key: x-ratelimit-remaining
9 value: "%DYNAMIC_METADATA(envoy.filters.http.ratelimit:remaining)%"Standard headers returned on 429:
X-RateLimit-Limit— the limit that appliesX-RateLimit-Remaining— tokens remainingX-RateLimit-Reset— seconds until bucket refillsRetry-After— seconds until the client can retry
Rate Limiting in Kubernetes: Envoy Gateway
Envoy Gateway is the Kubernetes Gateway API implementation backed by Envoy. It exposes rate limiting via a BackendTrafficPolicy CRD.
1# Install Envoy Gateway
2kubectl apply -f https://github.com/envoyproxy/gateway/releases/download/v1.0.0/install.yaml
3
4# GatewayClass
5apiVersion: gateway.networking.k8s.io/v1
6kind: GatewayClass
7metadata:
8 name: eg
9spec:
10 controllerName: gateway.envoyproxy.io/gatewayclass-controller
11---
12# Gateway
13apiVersion: gateway.networking.k8s.io/v1
14kind: Gateway
15metadata:
16 name: eg
17 namespace: default
18spec:
19 gatewayClassName: eg
20 listeners:
21 - name: http
22 protocol: HTTP
23 port: 80
24---
25# HTTPRoute
26apiVersion: gateway.networking.k8s.io/v1
27kind: HTTPRoute
28metadata:
29 name: my-api
30 namespace: default
31spec:
32 parentRefs:
33 - name: eg
34 hostnames:
35 - "api.example.com"
36 rules:
37 - matches:
38 - path:
39 type: PathPrefix
40 value: /
41 backendRefs:
42 - name: my-api-svc
43 port: 8080
44---
45# BackendTrafficPolicy — rate limiting
46apiVersion: gateway.envoyproxy.io/v1alpha1
47kind: BackendTrafficPolicy
48metadata:
49 name: ratelimit
50 namespace: default
51spec:
52 targetRef:
53 group: gateway.networking.k8s.io
54 kind: HTTPRoute
55 name: my-api
56 rateLimit:
57 type: Global
58 global:
59 rules:
60 - clientSelectors:
61 - headers:
62 - name: x-user-id
63 type: Distinct # Different limit per unique header value
64 limit:
65 requests: 100
66 unit: Minute
67 - limit:
68 requests: 1000
69 unit: Minute # Fallback global limitLocal rate limit via BackendTrafficPolicy
1spec:
2 rateLimit:
3 type: Local
4 local:
5 rules:
6 - limit:
7 requests: 100
8 unit: MinuteRate Limiting in Istio
If you're using Istio (which runs Envoy as sidecars), use EnvoyFilter to inject rate limit configuration:
1apiVersion: networking.istio.io/v1alpha3
2kind: EnvoyFilter
3metadata:
4 name: filter-ratelimit
5 namespace: istio-system
6spec:
7 workloadSelector:
8 labels:
9 istio: ingressgateway
10 configPatches:
11 - applyTo: HTTP_FILTER
12 match:
13 context: GATEWAY
14 listener:
15 filterChain:
16 filter:
17 name: envoy.filters.network.http_connection_manager
18 subFilter:
19 name: envoy.filters.http.router
20 patch:
21 operation: INSERT_BEFORE
22 value:
23 name: envoy.filters.http.ratelimit
24 typed_config:
25 "@type": type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit
26 domain: my-api
27 failure_mode_deny: false
28 rate_limit_service:
29 grpc_service:
30 envoy_grpc:
31 cluster_name: outbound|8081||ratelimit.istio-system.svc.cluster.local
32 transport_api_version: V3Monitoring Rate Limits
Envoy exposes rate limit metrics on its admin interface (default port 9901):
1# Check rate limit stats
2curl http://localhost:9901/stats | grep ratelimit
3
4# Key metrics
5# ratelimit.http_local_rate_limiter.rate_limited — requests rejected by local limiter
6# ratelimit.ok — global rate limit: allowed
7# ratelimit.over_limit — global rate limit: rejected
8# ratelimit.error — rate limit service unreachablePrometheus scrapes these via the /stats/prometheus endpoint.
Choosing Between Local and Global
| Local | Global | |
|---|---|---|
| Accuracy | Per-instance (approx.) | Exact across all instances |
| Latency overhead | None | +0.1–5ms (gRPC call) |
| Failure mode | Self-contained | Depends on rate limit service |
| Granularity | Route-level | Per header/IP/user |
| When to use | DDoS protection, coarse limits | Billing tiers, per-user quotas |
Frequently Asked Questions
What is the difference between local and global rate limiting?
Local limits are enforced per Envoy instance from its own counters, so with ten proxies the effective limit is ten times what you configured. Global limiting calls an external service holding shared counters, giving one true limit across the fleet at the cost of a dependency in the request path.
Which should I start with?
Local, because it needs no extra infrastructure and protects against the obvious case of one client overwhelming a single instance. Move to global when the limit must be exact across replicas, typically for per-tenant quotas you have committed to contractually.
What happens if the rate limit service is unavailable?
That is a configuration decision you should make deliberately. Failing open keeps traffic flowing and abandons the limit; failing closed enforces it and turns a limiter outage into an application outage. Most teams fail open for protective limits and closed for quota enforcement that must not be bypassed.
Should limits be per client or per route?
Both, usually. A per-route limit protects an expensive endpoint from aggregate load; a per-client descriptor stops one caller consuming the whole budget. Descriptors let you compose them, so start with the endpoint you most need to protect and add client dimensions as abuse patterns appear.
What's Next
- Kubernetes Networking and Ingress — the networking layer Envoy sits in front of
- Supply Chain Security: Sigstore and SLSA — secure the software that runs behind Envoy
Official References
- Istio traffic management — VirtualService, DestinationRule and gateway routing
- Linkerd architecture — control plane and proxy responsibilities
We built Podscape to simplify Kubernetes workflows like this — logs, events, and cluster state in one interface, without switching tools.
Struggling with this in production?
We help teams fix these exact issues. Our engineers have deployed these patterns across production environments at scale.