Rate limiting
Throttle clients per route with token buckets configured from ConfigMaps: keys, bursts and per-route overrides.
Use case: an API endpoint is getting hammered (a misbehaving client, a brute-force attempt, a scraper) and you want to throttle requests before they reach the backend.
This tutorial reuses the hello Service and edge Gateway from
Hello, HTTP.
1. Attach a rate limit
Rate limiting is an extension: a ConfigMap holding a
ratelimit.hcl document, attached to a route rule with an ExtensionRef
filter. The filter and the ConfigMap it points to travel together:
apiVersion: v1
kind: ConfigMap
metadata:
name: hello-limits
data:
ratelimit.hcl: |
version = 1
rate_limit {
requests = 3
window = "1m"
key = "client_ip"
}
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: hello
spec:
parentRefs:
- name: edge
hostnames:
- hello.example.com
rules:
- filters:
- type: ExtensionRef
extensionRef:
group: ""
kind: ConfigMap
name: hello-limits
backendRefs:
- name: hello
port: 80
Each (rule, client) pair gets a token bucket: requests tokens are
added per window, a request consumes one, and an empty bucket rejects.
The optional burst sets the bucket capacity (defaulting to requests)
for clients that legitimately arrive in spikes. Like the WAF, the filter
attaches per rule: only the matching rule is limited.
2. Trip it
Three requests per minute is easy to exhaust by hand:
GW_IP=$(kubectl get gateway edge -o jsonpath='{.status.addresses[0].value}')
for i in $(seq 1 5); do
curl -s -o /dev/null -w '%{http_code}\n' \
-H 'Host: hello.example.com' "http://$GW_IP/"
done
The first three print 200, the rest 429. A limited response tells
the client when to come back:
curl -i -H 'Host: hello.example.com' "http://$GW_IP/"
The Retry-After header carries the whole seconds until the next token.
The rejection status is configurable with status (any 4xx or 5xx) if
429 Too Many Requests does not fit your API.
3. Key by client identity
key = "client_ip" buckets by the resolved client address: the
downstream peer, or the client its forwarded chain names when the Gateway
trusts that peer.
When clients authenticate, bucket them by what identifies them instead:
version = 1
rate_limit {
requests = 3
window = "1m"
key = "header:X-API-Key"
}
Each distinct header value gets its own bucket; requests without the header share one anonymous bucket. Verify with two identities:
for key in alice bob; do
for i in $(seq 1 4); do
curl -s -o /dev/null -w "$key: %{http_code}\n" \
-H 'Host: hello.example.com' -H "X-API-Key: $key" "http://$GW_IP/"
done
done
alice and bob each get three 200s before their own 429.
4. Share a base, override per route
Several ExtensionRef filters on one rule compose in filter list order.
Unlike WAF directives (which concatenate), ratelimit.hcl documents
merge attribute by attribute: a later document overrides the attributes
it sets and inherits the rest. A platform-wide default can then be
tightened where it matters:
apiVersion: v1
kind: ConfigMap
metadata:
name: api-limits # shared by many routes
data:
ratelimit.hcl: |
version = 1
rate_limit {
requests = 100
window = "1m"
key = "client_ip"
}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: login-limits # stricter, for the login rule only
data:
ratelimit.hcl: |
version = 1
rate_limit {
requests = 5
}
filters:
- type: ExtensionRef
extensionRef:
group: ""
kind: ConfigMap
name: api-limits
- type: ExtensionRef
extensionRef:
group: ""
kind: ConfigMap
name: login-limits
The login rule ends up with requests = 5 and inherits window and
key from the base. The merged result must define at least requests
and window; an incomplete merge sets ResolvedRefs to False with
reason InvalidExtensionRef and the rule fails closed with 500,
exactly like a broken WAF program.
5. What to know in production
- Enforcement is local to each data-plane pod and there is no
cluster-wide coordination: with N pods behind one address, a client
can consume up to N times the configured rate. Size
requestsaccordingly. - Buckets are local to a configuration generation: editing the ConfigMap compiles a new one and counters start fresh, exactly like round-robin counters on a table swap. Idle buckets are reclaimed, so memory does not grow with the number of distinct clients.
- Rate limiting runs before the WAF (a limited request consumes no WAF CPU) and before any other filter; a limited request is never mirrored, redirected or forwarded.
- Decisions are observable as
krouter_dataplane_ratelimit_decisions_totaland in the access log; see Observability.