Platform Engineering

Envoy Proxy: Rate Limiting and Traffic Control

Advanced60 min to complete16 min readJune 1, 2026Updated August 19, 2026

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.

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
Rate Limiting
Service Mesh
Networking
Platform Engineering
API Gateway

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

yaml
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:

yaml
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: 60s

Global 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:

yaml
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: 8081

Rate limit service config

yaml
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: 50

Envoy filter config for global rate limiting

yaml
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: V3

Rate 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.

yaml
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 limit

Multiple 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:

yaml
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 applies
  • X-RateLimit-Remaining — tokens remaining
  • X-RateLimit-Reset — seconds until bucket refills
  • Retry-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.

yaml
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 limit

Local rate limit via BackendTrafficPolicy

yaml
1spec:
2  rateLimit:
3    type: Local
4    local:
5      rules:
6        - limit:
7            requests: 100
8            unit: Minute

Rate Limiting in Istio

If you're using Istio (which runs Envoy as sidecars), use EnvoyFilter to inject rate limit configuration:

yaml
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: V3

Monitoring Rate Limits

Envoy exposes rate limit metrics on its admin interface (default port 9901):

bash
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 unreachable

Prometheus scrapes these via the /stats/prometheus endpoint.


Choosing Between Local and Global

LocalGlobal
AccuracyPer-instance (approx.)Exact across all instances
Latency overheadNone+0.1–5ms (gRPC call)
Failure modeSelf-containedDepends on rate limit service
GranularityRoute-levelPer header/IP/user
When to useDDoS protection, coarse limitsBilling 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

Official References

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.