For the complete documentation index, see llms.txt. Markdown versions of all docs pages are available by appending .md to any docs URL.
Local rate limiting
Apply local and global rate limits to HTTP traffic to protect your backend services from overload.
Apply local and global rate limits to HTTP traffic to protect your backend services from overload.
About
Rate limiting in agentgateway protects your services from being overwhelmed by excessive traffic. A runaway automation script, a misconfigured retry loop, or a deliberate flood can exhaust your upstream’s capacity in seconds. Rate limiting gives you precise control over how much traffic reaches any route or the entire gateway — without any changes to the backend.
Rate limiting in agentgateway is expressed through AgentgatewayPolicy resources. A policy attaches to a Gateway or HTTPRoute target, and defines limits in the spec.traffic.rateLimit field. Gateway-level policies act as a hard ceiling on total traffic, while route-level policies provide finer-grained control.
Additionally, you can set up local or global rate limiting, depending on whether you want limits shared across Gateway instances.
| Mode | Where limits are enforced | Use case |
|---|---|---|
| Local | In-process, per proxy replica | Simple per-route or gateway-wide limits, and per-caller limits that do not need a shared count |
| Global | External rate limit service | Shared limits across multiple proxy replicas |
For AI-specific use cases, see:
Gateway-level global DoS protection
Target your Gateway resource to apply a limit across all routes. This acts as a hard ceiling on total gateway throughput regardless of which route is hit.
Example gateway policy
kubectl apply -f- <<EOF
apiVersion: agentgateway.dev/v1alpha1
kind: AgentgatewayPolicy
metadata:
name: gateway-rate-limit
namespace: agentgateway-system
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: agentgateway-proxy
traffic:
rateLimit:
local:
- requests: 5000
unit: Minutes
burst: 1000
EOFRoute-level rate limit
Route-level policies take precedence over gateway-level ones for their specific traffic.
Example route policy
kubectl apply -f- <<EOF
apiVersion: agentgateway.dev/v1alpha1
kind: AgentgatewayPolicy
metadata:
name: httpbin-rate-limit
namespace: httpbin
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: HTTPRoute
name: httpbin
traffic:
rateLimit:
local:
- requests: 3
unit: Seconds
burst: 3
EOFInheritance
Policies apply at the attachment point with a clear precedence order:
Gateway → Listener → Route → Route Rule → BackendMore specific policies win. A route-level limit overrides a gateway-level limit for traffic on that route.
With both policies in place, traffic to www.example.com is subject to the route limit (3 req/s), while all other routes are bounded only by the gateway limit (5000 req/min).
Response headers
When rate limiting is enabled, the following headers are added to responses. These headers help clients understand their current rate limit status and adapt their behavior accordingly.
Note: The x-envoy-ratelimited header is only present when using global rate limiting with an Envoy-compatible rate limit service. It is added by the rate limit service itself, not by agentgateway. As such, this header does not appear with local rate limiting.
| Header | Description | Added by | Example |
|---|---|---|---|
x-ratelimit-limit | The rate limit ceiling for the given request. For local rate limiting, this is the base limit plus burst. For global rate limiting with time windows, this might include window information. | Agentgateway | 6 (local), 10, 10;w=60 (global with 60-second window) |
x-ratelimit-remaining | The number of requests (or tokens for LLM rate limiting) remaining in the current time window. | Agentgateway | 5 |
x-ratelimit-reset | The time in seconds until the rate limit window resets. | Agentgateway | 30 |
x-envoy-ratelimited | Present when the request is rate limited. Only appears in 429 responses when using global rate limiting. | External rate limit service | (header present) |
Before you begin
Follow the Get started guide to install agentgateway.
Follow the Sample app guide to create a gateway proxy with an HTTP listener and deploy the httpbin sample app.
Get the external address of the gateway and save it in an environment variable.
Tip
Kind cluster? Kind does not support
LoadBalancerservices by default. To use this option with a Kind cluster, install and runcloud-provider-kind.export INGRESS_GW_ADDRESS=$(kubectl get svc -n agentgateway-system agentgateway-proxy -o jsonpath="{.status.loadBalancer.ingress[0]['hostname','ip']}") echo $INGRESS_GW_ADDRESS
Local rate limiting
Local rate limiting runs entirely inside the agentgateway proxy — no external service needed. The following steps show how to apply request-based limits to your HTTP traffic.
Apply a rate limit to the httpbin HTTPRoute.
Review the following table to understand this configuration.kubectl apply -f- <<EOF apiVersion: agentgateway.dev/v1alpha1 kind: AgentgatewayPolicy metadata: name: httpbin-rate-limit namespace: httpbin spec: targetRefs: - group: gateway.networking.k8s.io kind: HTTPRoute name: httpbin traffic: rateLimit: local: - requests: 3 unit: Seconds burst: 3 EOFField Required Description requestsYes Number of requests allowed per unit.unitYes Seconds,Minutes, orHours.burstNo Extra requests allowed above the base rate in a short burst. The burstfield implements a token bucket on top of the base rate. Withrequests: 3, burst: 3, you get up to 6 requests in one burst (3 base + 3 burst capacity), then the bucket refills at 3 per second. This absorbs short traffic spikes without rejecting requests. This setting only works withrequests, not withtokenrate limits.keyNo CEL expression that selects the bucket a request counts against, such as the jwt.subclaim for a limit per user. Each distinct value gets its own bucket with the limits above. When unset, all requests on the target share one bucket. For more information, see Claim-level rate limits.Verify that the policy is attached.
kubectl get AgentgatewayPolicy httpbin-rate-limit -n httpbin \ -o jsonpath='{.status.ancestors[0].conditions}' | jq .A healthy policy reports both
AcceptedandAttachedasTrue:[ { "type": "Accepted", "status": "True", "message": "Policy accepted" }, { "type": "Attached", "status": "True", "message": "Attached to all targets" } ]If
AttachedisFalse, the policy’stargetRefpoints to a resource that doesn’t exist. Check themessagefield for the exact resource name that’s missing.Fire 10 rapid requests to test the rate limit.
for i in $(seq 1 10); do STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ http://$INGRESS_GW_ADDRESS:80/headers -H "host: www.example.com") echo "Request $i: HTTP $STATUS" doneExample output:
Request 1: HTTP 200 Request 2: HTTP 200 Request 3: HTTP 200 Request 4: HTTP 200 Request 5: HTTP 200 Request 6: HTTP 200 Request 7: HTTP 429 Request 8: HTTP 429 Request 9: HTTP 429 Request 10: HTTP 429The first 6 succeed (3 base + 3 burst), then requests are rejected until the bucket refills. Inspect a 429 response to see the rate limit headers:
HTTP/1.1 429 Too Many Requests x-ratelimit-limit: 6 x-ratelimit-remaining: 0 x-ratelimit-reset: 0 content-type: text/plain content-length: 19 rate limit exceededAfter 1 second the bucket refills and requests succeed again.
sleep 1 && curl -o /dev/null -w "%{http_code}\n" \ localhost:8080/headers -H "host: www.example.com" # 200
Claim-level rate limits
Create claim-level rate limits with CEL expressions.
The limit that you applied in the previous section is shared by every request on the route, so one busy client can exhaust it for everyone else. To limit each caller separately, set the key field to a CEL expression. Each distinct value that the expression returns gets its own token bucket with the limits of that rule. The expression typically reads a claim in a JWT, such as jwt.sub for a limit per user, jwt.team for a limit per team, or jwt.sub + "/" + request.path for a limit per user per path. This way, you can enforce claim-level limits without an external rate limit service.
In production, key the limit on a value that the client cannot choose, which means a claim from a JWT authentication policy that targets the same route.
rateLimit:
local:
- requests: 2
unit: Minutes
key: jwt.subThe following steps key the limit on a request header instead, so that you can see the behavior without an identity provider.
Update the
httpbin-rate-limitpolicy from the previous section to key the limit on a user header. The rest of the policy is unchanged, so the route keeps one policy rather than gaining a second one.kubectl apply -f- <<EOF apiVersion: agentgateway.dev/v1alpha1 kind: AgentgatewayPolicy metadata: name: httpbin-rate-limit namespace: httpbin spec: targetRefs: - group: gateway.networking.k8s.io kind: HTTPRoute name: httpbin traffic: rateLimit: local: - requests: 2 unit: Minutes key: 'request.headers["x-user"]' EOFSend three requests as one user. The first two succeed, and the third is rejected.
for i in $(seq 1 3); do curl -s -o /dev/null -w "alice request $i: HTTP %{http_code}\n" \ http://$INGRESS_GW_ADDRESS:80/headers -H "host: www.example.com" -H "x-user: alice" doneExample output:
alice request 1: HTTP 200 alice request 2: HTTP 200 alice request 3: HTTP 429Send requests as a second user. These requests succeed, because each user has an independent bucket.
for i in $(seq 1 3); do curl -s -o /dev/null -w "bob request $i: HTTP %{http_code}\n" \ http://$INGRESS_GW_ADDRESS:80/headers -H "host: www.example.com" -H "x-user: bob" doneExample output:
bob request 1: HTTP 200 bob request 2: HTTP 200 bob request 3: HTTP 429
Review the following behavior before you rely on a claim-level limit.
- Requests without a value: Requests whose key is empty, or whose expression cannot be evaluated, such as a request with no
x-userheader in this example, all share one bucket. An empty key does not exempt a request from the limit. To apply a limit to only some requests, use conditional policies instead. - How many buckets are kept, and where they live: Each rule keeps up to 65,536 buckets and drops the least used ones, which for that key is the same as never having been seen. Buckets are held in memory by a single proxy replica, so each replica enforces the limit separately. For a limit that is shared across replicas, use global rate limiting.
- Invalid expressions: If the expression does not compile, the policy is accepted with the
PartiallyValidreason, and the rest of the policy still applies. Check the policy status for the messagelocal rate limit key is not a valid CEL expression.
For the variables that you can read in a key, see Variables and functions.
Global rate limiting
Local rate limiting runs independently on each proxy replica. If you run multiple agentgateway replicas and need a shared quota across the fleet, use global rate limiting backed by an external service such as Envoy’s rate limit service.
For detailed instructions on setting up global rate limiting with descriptors and an external rate limit service, see the Global rate limiting guide.
Conditional execution
To apply different rate limits based on the request, use the conditional field on your rateLimit policy. For example, you can apply stricter limits on writes than on reads. For details, see Conditional policies.
Cleanup
You can remove the resources that you created in this guide.kubectl delete AgentgatewayPolicy httpbin-rate-limit -n httpbinApply more than one local limit
The local field takes a list, so one policy can carry several limits. Every entry in the list is enforced, and a request is rejected with a 429 response as soon as any one of them is exhausted.
Use more than one entry to combine a short window that absorbs a burst with a long window that caps sustained volume.
kubectl apply -f- <<EOF
apiVersion: agentgateway.dev/v1alpha1
kind: AgentgatewayPolicy
metadata:
name: httpbin-rate-limit
namespace: httpbin
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: HTTPRoute
name: httpbin
traffic:
rateLimit:
local:
# Short window: smooth out bursts
- requests: 10
unit: Seconds
burst: 5
# Long window: cap sustained volume
- requests: 100
unit: Minutes
EOFIn this example, a client can send 10 requests per second, and no more than 100 requests per minute. A client that sends 10 requests per second continuously is rejected once it reaches 100 requests in the minute, even though it never exceeds the per-second limit.
Important
In version 1.4 and earlier, the Kubernetes controller sent only the first entry of the list to the proxy, so a second and later entry was accepted but never enforced. Version 1.5 enforces every entry. If you already have a policy with more than one entry, review it before you upgrade, because a limit that had no effect starts rejecting traffic. Standalone mode enforced every entry in earlier versions as well.
Each entry is independent, and each keeps its own counter. Local rate limits run per proxy replica, so the effective limit across a deployment is the configured limit multiplied by the replica count. To share counters across replicas, use global rate limiting instead.