APISIX rate limiting and JWT claim routing
Published: 2026-03-03
Apache APISIX's real power over nginx-based ingresses is the plugin system. Where nginx requires Lua modules and complex config rewrites, APISIX routes, rate-limits, and transforms requests with a few lines of YAML via Kubernetes CRDs. Here's how we use it in practice.
ApisixRoute: the basic unit
yamlapiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
name: api-route
namespace: app
spec:
http:
- name: main
match:
hosts:
- api.example.com
paths:
- /*
backends:
- serviceName: my-api-service
servicePort: 80
plugins:
- name: redirect
enable: true
config:
http_to_https: true
Routes are per-spec.http entry. Multiple entries on one ApisixRoute, or multiple ApisixRoute objects per host — both work.
Rate limiting per agency from JWT claim
One pattern we use heavily: extract a claim from the JWT and use it as the rate limit key, so different clients get different limits.
yamlplugins:
# 1. Global rate limit (all clients combined)
- name: limit-count
enable: true
config:
count: 17
time_window: 1
rejected_code: 429
key_type: constant
# 2. Per-client limit via JWT agency-id claim
- name: serverless-pre-function
enable: true
config:
phase: rewrite
functions:
- |-
return function(conf, ctx)
local core = require("apisix.core")
local jwt = require("resty.jwt")
local token = core.request.header(ctx, "Authorization")
if token then
local _, _, t = string.find(token, "Bearer%s+(.+)")
if t then
local obj = jwt:load_jwt(t)
if obj.valid then
local agency = obj.payload["agency-id"]
core.request.set_header(ctx, "X-Agency-Id", agency)
end
end
end
end
The Lua serverless function runs in the rewrite phase — before the request is proxied. It reads the JWT, extracts agency-id, and sets a header that downstream plugins can use for routing decisions.
Rate limit by agency
yaml # 3. Limit-count per agency (using the extracted header)
- name: limit-count
enable: true
config:
count: 100
time_window: 60
rejected_code: 429
key_type: var
key: http_x_agency_id
group: agency-rate-limit
Each unique X-Agency-Id value gets its own counter. VIP agencies can be excluded by checking the header value in the Lua function first.
Per-path routing with regex
Route different API paths to different backends:
yamlspec:
http:
- name: airshopping
match:
hosts: [api.example.com]
paths: [/*]
exprs:
- subject:
scope: Path
op: RegexMatchCaseInsensitive
value: "^/api/order/(airshopping|offerprice)$"
backends:
- serviceName: search-service
servicePort: 80
- name: booking
match:
hosts: [api.example.com]
paths: [/api/order/createorder]
backends:
- serviceName: booking-service
servicePort: 80
- name: fallback
match:
hosts: [api.example.com]
paths: [/*]
priority: -1
backends:
- serviceName: default-service
servicePort: 80
exprs provide predicate matching on path, headers, or query params. RegexMatchCaseInsensitive is the most flexible option. Use priority to control which rule wins when multiple paths match.
ApisixUpstream: health checks and load balancing
yamlapiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
name: my-api-upstream
namespace: app
spec:
loadbalancer:
type: roundrobin
healthCheck:
active:
type: http
httpPath: /healthz
interval: 10
timeout: 2
successThreshold: 1
failureThreshold: 3
httpStatusCode: [200]
externalNodes:
- type: Service
name: my-api-service
port: 80
weight: 100
Active health checks probe /healthz every 10 seconds. After 3 consecutive failures, the upstream is marked unhealthy and removed from rotation. APISIX handles this in the data plane without involving Kubernetes.
Traffic splitting for canary releases
yamlspec:
http:
- name: canary
match:
hosts: [api.example.com]
paths: [/*]
backends:
- serviceName: api-stable
servicePort: 80
weight: 90
- serviceName: api-canary
servicePort: 80
weight: 10
10% of traffic goes to the canary version. No additional tooling — just a weight ratio in the route config. Adjust weights via GitOps or a kubectl patch.
Key-auth plugin for API key access
yamlplugins:
- name: key-auth
enable: true
config:
header: "X-API-Key"
query: "apikey"
Then create an ApisixConsumer with the allowed key:
yamlapiVersion: apisix.apache.org/v2
kind: ApisixConsumer
metadata:
name: my-client
namespace: app
spec:
authParameter:
keyAuth:
value:
key: "my-secret-api-key"
Requests without a valid X-API-Key header return 401 at the gateway — the upstream service never sees unauthenticated traffic.
Request transformation
Strip headers before forwarding to upstream, or add custom ones:
yamlplugins:
- name: proxy-rewrite
enable: true
config:
headers:
remove:
- Authorization # don't forward auth token to upstream
- X-Agency-Id
add:
X-Forwarded-Service: "api-gateway"
regex_uri:
- "^/api/v1/(.*)"
- "/internal/$1" # rewrite path
Debugging routes
bash# List all ApisixRoutes
kubectl get apisixroute -n app
# Check route details
kubectl describe apisixroute api-route -n app
# Check APISIX data plane routes via admin API
kubectl port-forward -n ingress-apisix svc/apisix-admin 9080:9080
curl -s http://localhost:9080/apisix/admin/routes | jq '[.list[] | {name: .value.name, uri: .value.uri}]'