limit-conn
The limit-conn plugin limits concurrent HTTP requests, WebSocket connections, and TCP connections on stream routes. Traffic above the configured threshold can be delayed or rejected to control resource use and prevent overload.
Local vs Redis Rate Limiting
For HTTP routes, the limit-conn plugin supports two modes of rate limiting:
- Local rate limiting: Limits are enforced independently on each gateway instance. Each instance maintains its own counters, so the effective limit is roughly (limit × number of instances) when traffic is spread across instances. This is the default when no
policyis set or whenpolicyislocal. - Redis-based rate limiting: Limits are shared across all gateway instances through Redis. All instances share the same quota, so the configured limit applies to all gateway instances.
The stream plugin schema does not define policy, so the documented stream configuration uses local counters. See Limit TCP Connections on a Stream Route for the stream parameters and an example.
Examples
The following scenarios configure local and Redis-backed connection limits for HTTP, WebSocket, and stream routes.
Apply Rate Limiting by Remote Address
This example uses remote_addr as the rate-limit key, so each client address receives its own connection and burst thresholds.
Create a route that limits connections by client address:
curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "limit-conn-route",
"uri": "/get",
"plugins": {
"limit-conn": {
"conn": 2,
"burst": 1,
"default_conn_delay": 0.1,
"key_type": "var",
"key": "remote_addr",
"policy": "local",
"rejected_code": 429
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'services:
- name: httpbin
labels:
docs-example: limit-conn-remote-address
routes:
- uris:
- /get
name: limit-conn-route
plugins:
limit-conn:
conn: 2
burst: 1
default_conn_delay: 0.1
key_type: var
key: remote_addr
policy: local
rejected_code: 429
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1Preview changes owned by this example and confirm that the diff contains no unintended updates or deletions:
adc diff -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=limit-conn-remote-addressSynchronize the reviewed service configuration:
adc sync -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=limit-conn-remote-addressapiVersion: v1
kind: Service
metadata:
namespace: aic
name: httpbin-external-domain
spec:
type: ExternalName
externalName: httpbin.org
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: limit-conn-plugin-config
spec:
plugins:
- name: limit-conn
config:
conn: 2
burst: 1
default_conn_delay: 0.1
key_type: var
key: remote_addr
policy: local
rejected_code: 429
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: limit-conn-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /get
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: limit-conn-plugin-config
backendRefs:
- name: httpbin-external-domain
port: 80apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: httpbin-external-domain
spec:
ingressClassName: apisix
externalNodes:
- type: Domain
name: httpbin.org
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: limit-conn-route
spec:
ingressClassName: apisix
http:
- name: limit-conn-route
match:
paths:
- /get
methods:
- GET
upstreams:
- name: httpbin-external-domain
plugins:
- name: limit-conn
config:
conn: 2
burst: 1
default_conn_delay: 0.1
key_type: var
key: remote_addr
policy: local
rejected_code: 429Apply the configuration:
kubectl apply -f limit-conn-ic.yaml❶ conn: allow 2 concurrent requests.
❷ burst: allow 1 excessive concurrent request.
❸ default_conn_delay: Allow 0.1 second of processing latency for concurrent requests between conn and conn + burst.
❹ key_type: set to var to interpret key as a variable.
❺ key: calculate rate limiting count by request's remote_addr.
❻ policy: use the local counter in memory.
❼ rejected_code: set the rejection status code to 429.
Send five concurrent requests to the route:
seq 1 5 | xargs -n1 -P5 bash -c 'curl -s -o /dev/null -w "Response: %{http_code}\n" "http://127.0.0.1:9080/get"'You should see responses similar to the following, where excessive requests are rejected:
Response: 200
Response: 200
Response: 200
Response: 429
Response: 429Apply Rate Limiting by Remote Address and Consumer Name
This example combines remote_addr and consumer_name in the rate-limit key, giving each consumer an independent quota for a client address.
Create consumer john:
curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"username": "john"
}'Create key-auth credential for the consumer:
curl "http://127.0.0.1:9180/apisix/admin/consumers/john/credentials" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "cred-john-key-auth",
"plugins": {
"key-auth": {
"key": "john-key"
}
}
}'Create a second consumer jane:
curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"username": "jane"
}'Create key-auth credential for the consumer:
curl "http://127.0.0.1:9180/apisix/admin/consumers/jane/credentials" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "cred-jane-key-auth",
"plugins": {
"key-auth": {
"key": "jane-key"
}
}
}'Create a route with key-auth and limit-conn plugins:
curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "limit-conn-route",
"uri": "/get",
"plugins": {
"key-auth": {},
"limit-conn": {
"conn": 2,
"burst": 1,
"default_conn_delay": 0.1,
"rejected_code": 429,
"policy": "local",
"key_type": "var_combination",
"key": "$remote_addr $consumer_name"
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'Create two consumers and a route that enables rate limiting by consumers:
consumers:
- username: john
labels:
docs-example: limit-conn-consumer
credentials:
- name: key-auth
type: key-auth
config:
key: john-key
- username: jane
labels:
docs-example: limit-conn-consumer
credentials:
- name: key-auth
type: key-auth
config:
key: jane-key
services:
- name: limit-conn-service
labels:
docs-example: limit-conn-consumer
routes:
- name: limit-conn-route
uris:
- /get
plugins:
key-auth: {}
limit-conn:
conn: 2
burst: 1
default_conn_delay: 0.1
rejected_code: 429
policy: local
key_type: var_combination
key: "$remote_addr $consumer_name"
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1Preview changes owned by this example and confirm that the diff contains no unintended updates or deletions:
adc diff -f adc.yaml \
--include-resource-type service \
--include-resource-type consumer \
--label-selector docs-example=limit-conn-consumerSynchronize the reviewed service and consumer configurations:
adc sync -f adc.yaml \
--include-resource-type service \
--include-resource-type consumer \
--label-selector docs-example=limit-conn-consumerCreate two consumers and a route that enables rate limiting by consumers:
apiVersion: apisix.apache.org/v1alpha1
kind: Consumer
metadata:
namespace: aic
name: john
spec:
gatewayRef:
name: apisix
credentials:
- type: key-auth
name: primary-key
config:
key: john-key
---
apiVersion: apisix.apache.org/v1alpha1
kind: Consumer
metadata:
namespace: aic
name: jane
spec:
gatewayRef:
name: apisix
credentials:
- type: key-auth
name: primary-key
config:
key: jane-key
---
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: httpbin-external-domain
spec:
type: ExternalName
externalName: httpbin.org
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: limit-conn-plugin-config
spec:
plugins:
- name: key-auth
config:
_meta:
disable: false
- name: limit-conn
config:
conn: 2
burst: 1
default_conn_delay: 0.1
rejected_code: 429
policy: local
key_type: var_combination
key: "$remote_addr $consumer_name"
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: limit-conn-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /get
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: limit-conn-plugin-config
backendRefs:
- name: httpbin-external-domain
port: 80apiVersion: apisix.apache.org/v2
kind: ApisixConsumer
metadata:
namespace: aic
name: john
spec:
ingressClassName: apisix
authParameter:
keyAuth:
value:
key: john-key
---
apiVersion: apisix.apache.org/v2
kind: ApisixConsumer
metadata:
namespace: aic
name: jane
spec:
ingressClassName: apisix
authParameter:
keyAuth:
value:
key: jane-key
---
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: httpbin-external-domain
spec:
ingressClassName: apisix
externalNodes:
- type: Domain
name: httpbin.org
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: limit-conn-route
spec:
ingressClassName: apisix
http:
- name: limit-conn-route
match:
paths:
- /get
methods:
- GET
upstreams:
- name: httpbin-external-domain
plugins:
- name: key-auth
config:
_meta:
disable: false
- name: limit-conn
config:
conn: 2
burst: 1
default_conn_delay: 0.1
rejected_code: 429
policy: local
key_type: var_combination
key: "$remote_addr $consumer_name"Apply the configuration:
kubectl apply -f limit-conn-ic.yaml❶ key-auth: enable key authentication on the route.
❷ key_type: set to var_combination to interpret the key as a combination of variables.
❸ key: set to $remote_addr $consumer_name to apply rate limiting quota by remote address and consumer.
Send five concurrent requests as the consumer john:
seq 1 5 | xargs -n1 -P5 bash -c 'curl -s -o /dev/null -w "Response: %{http_code}\n" "http://127.0.0.1:9080/get" -H "apikey: john-key"'You should see responses similar to the following, where excessive requests are rejected:
Response: 200
Response: 200
Response: 200
Response: 429
Response: 429Immediately send five concurrent requests as the consumer jane:
seq 1 5 | xargs -n1 -P5 bash -c 'curl -s -o /dev/null -w "Response: %{http_code}\n" "http://127.0.0.1:9080/get" -H "apikey: jane-key"'You should also see responses similar to the following, where excessive requests are rejected:
Response: 200
Response: 200
Response: 200
Response: 429
Response: 429Matching results for both consumers confirm that each consumer has an independent quota.
Rate Limit WebSocket Connections
This example limits the number of concurrent WebSocket connections from each client address. It uses a pinned echo server so the upstream is reproducible in Docker and Kubernetes.
Start a sample upstream WebSocket server:
Set GATEWAY_CONTAINER to the running APISIX or API7 Gateway container. Create a dedicated network and connect the gateway to it:
export GATEWAY_CONTAINER=replace-with-gateway-container-name
docker network create gateway-websocket-net
docker network connect gateway-websocket-net "$GATEWAY_CONTAINER"Start the echo server on the shared network:
docker run -d \
--name websocket-server \
--network gateway-websocket-net \
jmalloc/echo-server:v0.3.7Set the upstream hostname used by the Admin API and ADC examples:
export WEBSOCKET_UPSTREAM_HOST=websocket-serverCreate the echo server Deployment and Service:
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: aic
name: websocket-server
spec:
replicas: 1
selector:
matchLabels:
app: websocket-server
template:
metadata:
labels:
app: websocket-server
spec:
containers:
- name: echo-server
image: jmalloc/echo-server:v0.3.7
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: websocket-server
spec:
selector:
app: websocket-server
ports:
- protocol: TCP
port: 8080
targetPort: 8080
appProtocol: kubernetes.io/ws
type: ClusterIPApply the manifest:
kubectl apply -f websocket-server.yaml
kubectl rollout status deployment/websocket-server -n aicSet the upstream hostname used by the Admin API and ADC examples:
export WEBSOCKET_UPSTREAM_HOST=websocket-server.aic.svcGateway API and WebSocket
For Gateway API, WebSocket support is enabled through the Service's appProtocol field (kubernetes.io/ws or kubernetes.io/wss). Unlike ApisixRoute, there is no direct websocket field or annotation support in HTTPRoute. Ensure that your Service is configured with appProtocol if you are working with Gateway API resources.
See Detect Upstream Protocol with appProtocol (opens in Ingress Controller docs) for more information.
The server exposes /.ws, which echoes each received message.
Create a route to the server WebSocket endpoint and enable WebSocket for the route:
curl "http://127.0.0.1:9180/apisix/admin/routes/ws-limit-conn" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d @- <<EOF
{
"uri": "/.ws",
"plugins": {
"limit-conn": {
"conn": 2,
"burst": 1,
"default_conn_delay": 0.1,
"key_type": "var",
"key": "remote_addr",
"rejected_code": 429,
"policy": "local"
}
},
"enable_websocket": true,
"upstream": {
"type": "roundrobin",
"nodes": {
"$WEBSOCKET_UPSTREAM_HOST:8080": 1
}
}
}
EOFservices:
- name: websocket-service
labels:
docs-example: limit-conn-websocket
routes:
- name: ws-route
uris:
- /.ws
enable_websocket: true
plugins:
limit-conn:
conn: 2
burst: 1
default_conn_delay: 0.1
key_type: var
key: remote_addr
rejected_code: 429
policy: local
upstream:
type: roundrobin
nodes:
- host: "${WEBSOCKET_UPSTREAM_HOST}"
port: 8080
weight: 1Preview changes owned by this example and confirm that the diff contains no unintended updates or deletions:
adc diff -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=limit-conn-websocketSynchronize the reviewed service configuration:
adc sync -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=limit-conn-websocketapiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: limit-conn-plugin-config
spec:
plugins:
- name: limit-conn
config:
conn: 2
burst: 1
default_conn_delay: 0.1
key_type: var
key: remote_addr
rejected_code: 429
policy: local
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: ws-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /.ws
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: limit-conn-plugin-config
backendRefs:
- name: websocket-server
port: 8080apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: ws-route
spec:
ingressClassName: apisix
http:
- name: ws-route
match:
paths:
- /.ws
methods:
- GET
websocket: true
backends:
- serviceName: websocket-server
servicePort: 8080
plugins:
- name: limit-conn
config:
conn: 2
burst: 1
default_conn_delay: 0.1
key_type: var
key: remote_addr
rejected_code: 429
policy: localApply the configuration:
kubectl apply -f limit-conn-ic.yamlInstall a WebSocket client, such as websocat, if you have not already. Open a connection through the route:
websocat "ws://127.0.0.1:9080/.ws"Send hello. The server should echo the message:
Request served by 1cd244052136
hello
helloKeep the first connection open. Open three more terminal sessions and run the same command in each one:
websocat "ws://127.0.0.1:9080/.ws"The fourth connection should fail with 429 Too Many Requests because two connections are active and one additional connection already occupies the burst capacity.
Limit TCP Connections on a Stream Route
This example limits concurrent TCP connections from each client address on an APISIX stream route (opens in Apache APISIX docs). It allows one active connection per client and closes additional connections until the active connection ends.
Before creating the stream route, configure APISIX to listen for TCP traffic on port 9100.
Add or update this section in the gateway configuration file:
apisix:
proxy_mode: http&stream
stream_proxy:
tcp:
- 9100For a host installation, reload APISIX after updating the configuration:
apisix reloadFor the APISIX quickstart container, publish port 9100 from the container to the host, such as with -p 9100:9100, when creating or redeploying the container. Then reload APISIX in the container:
docker exec apisix-quickstart apisix reloadAdd the TCP listener to the existing APISIX Helm values file:
service:
stream:
enabled: true
tcp:
- 9100These values configure APISIX stream mode and expose TCP port 9100 through the gateway Service.
Apply the values file with the chart used for the deployment:
helm upgrade <release-name> apisix/apisix \
--version <chart-version> \
--namespace <namespace> \
-f values.yamlList the Services created for the Helm release:
kubectl get services \
--namespace <namespace> \
-l app.kubernetes.io/instance=<release-name>For local verification, use the Admin API Service name from the output to forward its port in one terminal:
kubectl port-forward service/<admin-service-name> \
9180:9180 \
--namespace <namespace>In another terminal, use the gateway Service name to forward the stream proxy port:
kubectl port-forward service/<gateway-service-name> \
9100:9100 \
--namespace <namespace>Keep both port-forward sessions running while you complete the example.
See Proxy Transport Layer (L4) Traffic (opens in Apache APISIX docs) for more information about exposing stream listeners.
Create a stream route that allows one connection at a time from each client address and proxies TCP traffic to an HTTP-speaking test service:
curl "http://127.0.0.1:9180/apisix/admin/stream_routes/limit-conn-tcp" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"server_port": 9100,
"plugins": {
"limit-conn": {
"conn": 1,
"burst": 0,
"default_conn_delay": 0.1,
"key_type": "var",
"key": "remote_addr"
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'services:
- name: httpbin-stream
labels:
docs-example: limit-conn-stream
upstream:
name: default
scheme: tcp
nodes:
- host: httpbin.org
port: 80
weight: 1
stream_routes:
- name: limit-conn-tcp
server_port: 9100
plugins:
limit-conn:
conn: 1
burst: 0
default_conn_delay: 0.1
key_type: var
key: remote_addrPreview changes owned by this example and confirm that the diff contains no unintended updates or deletions:
adc diff -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=limit-conn-streamSynchronize the reviewed stream service configuration:
adc sync -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=limit-conn-streamEnsure that the Gateway has a TCP listener named tcp on port 9100, as shown in Proxy TCP Traffic by Port (opens in Ingress Controller docs). Then create the upstream Service and TCPRoute, and attach the plugin with an L4RoutePolicy:
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: httpbin-stream
spec:
type: ExternalName
externalName: httpbin.org
ports:
- name: tcp
port: 80
---
apiVersion: gateway.networking.k8s.io/v1
kind: TCPRoute
metadata:
namespace: aic
name: limit-conn-tcp
spec:
parentRefs:
- name: apisix
sectionName: tcp
rules:
- backendRefs:
- name: httpbin-stream
port: 80
---
apiVersion: apisix.apache.org/v1alpha1
kind: L4RoutePolicy
metadata:
namespace: aic
name: limit-conn-tcp
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: TCPRoute
name: limit-conn-tcp
plugins:
- name: limit-conn
config:
conn: 1
burst: 0
default_conn_delay: 0.1
key_type: var
key: remote_addrCreate the upstream Service and an APISIX CRD stream route with the plugin:
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: httpbin-stream
spec:
type: ExternalName
externalName: httpbin.org
ports:
- name: tcp
port: 80
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: limit-conn-tcp
spec:
ingressClassName: apisix
stream:
- name: limit-conn-tcp
protocol: TCP
match:
ingressPort: 9100
backend:
serviceName: httpbin-stream
servicePort: 80
plugins:
- name: limit-conn
enable: true
config:
conn: 1
burst: 0
default_conn_delay: 0.1
key_type: var
key: remote_addrApply the configuration:
kubectl apply -f limit-conn-ic.yaml❶ conn: allow one concurrent TCP connection for each client address.
❷ burst: set the burst capacity to zero so connections over the limit are rejected immediately instead of delayed.
❸ default_conn_delay: set the required base delay to 0.1 seconds. This example does not delay excess connections because burst is zero.
❹ key_type: interpret the configured key as a variable.
❺ key: count connections separately for each client address using remote_addr.
Open a TCP connection to the stream route and leave the session open:
nc 127.0.0.1 9100In another terminal, try to open a second connection from the same client address:
curl -i --max-time 5 \
-H "Host: httpbin.org" \
"http://127.0.0.1:9100/get"The second connection is rejected, and the terminal displays:
curl: (52) Empty reply from serverDepending on the client and operating system, the message might instead report Connection reset by peer.
Close the nc session, then run the request again:
curl -i --max-time 5 \
-H "Host: httpbin.org" \
"http://127.0.0.1:9100/get"The connection now succeeds because the slot is available again:
HTTP/1.1 200 OK
Content-Type: application/json
...
{
"url": "http://httpbin.org/get"
}API7 Gateway uses stream services and Gateway Group-scoped Admin API requests. See Configure TCP/UDP Proxying (opens in API7 Gateway docs) for its limit-conn stream-route configuration workflow.
Share Quota Among Gateway Nodes with a Redis Server
This example stores connection counters in one Redis server so gateway nodes that load the same route resource enforce one shared limit.
Start Redis
Start Redis in the environment used by the gateway.
Set GATEWAY_CONTAINER to the running APISIX or API7 Gateway container. Create a dedicated network and connect the gateway to it:
export GATEWAY_CONTAINER=replace-with-gateway-container-name
docker network create gateway-redis-net
docker network connect gateway-redis-net "$GATEWAY_CONTAINER"Repeat the network connection for every additional gateway container used to verify the shared counter:
docker network connect gateway-redis-net replace-with-additional-gateway-containerStart Redis on the shared network:
docker run -d \
--name redis-standalone \
--network gateway-redis-net \
redis:8.10.2-alpine \
redis-server --requirepass redis-passwordVerify the authenticated connection:
docker exec -e REDISCLI_AUTH=redis-password redis-standalone redis-cli pingYou should receive PONG.
Set the Redis hostname used by the Admin API and ADC examples:
export REDIS_HOST=redis-standaloneCreate a Redis Deployment and Service:
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: aic
name: redis-standalone
spec:
replicas: 1
selector:
matchLabels:
app: redis-standalone
template:
metadata:
labels:
app: redis-standalone
spec:
containers:
- name: redis
image: redis:8.10.2-alpine
args:
- redis-server
- --requirepass
- redis-password
ports:
- containerPort: 6379
---
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: redis-standalone
spec:
selector:
app: redis-standalone
ports:
- port: 6379
targetPort: 6379Apply the manifest and wait for Redis to be ready:
kubectl apply -f redis-standalone.yaml
kubectl rollout status deployment/redis-standalone -n aicSet the Redis hostname used by the Admin API and ADC examples:
export REDIS_HOST=redis-standalone.aic.svcThese fixtures use tutorial passwords and internal service addresses. For production, restrict network access, manage credentials as secrets, enable persistence, and configure TLS where supported.
Create a Route
Create the route in the gateway cluster or gateway group. Each node must load the same route resource and be able to reach Redis. The route sends requests to httpbin's two-second delay endpoint so connections overlap during verification.
curl "http://127.0.0.1:9180/apisix/admin/routes/limit-conn-redis" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d @- <<EOF
{
"uri": "/limit-conn/redis",
"plugins": {
"proxy-rewrite": {
"uri": "/delay/2"
},
"limit-conn": {
"conn": 1,
"burst": 1,
"default_conn_delay": 0.1,
"rejected_code": 429,
"key_type": "var",
"key": "remote_addr",
"policy": "redis",
"redis_host": "$REDIS_HOST",
"redis_password": "redis-password"
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}
EOFservices:
- name: limit-conn-redis-service
labels:
docs-example: limit-conn-redis
routes:
- name: limit-conn-redis
uris:
- /limit-conn/redis
plugins:
proxy-rewrite:
uri: /delay/2
limit-conn:
conn: 1
burst: 1
default_conn_delay: 0.1
rejected_code: 429
key_type: var
key: remote_addr
policy: redis
redis_host: "${REDIS_HOST}"
redis_password: redis-password
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1Preview changes owned by this example and confirm that the diff contains no unintended updates or deletions:
adc diff -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=limit-conn-redisSynchronize the reviewed service configuration:
adc sync -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=limit-conn-redisapiVersion: v1
kind: Service
metadata:
namespace: aic
name: httpbin-external-domain
spec:
type: ExternalName
externalName: httpbin.org
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: limit-conn-redis-plugin-config
spec:
plugins:
- name: proxy-rewrite
config:
uri: /delay/2
- name: limit-conn
config:
conn: 1
burst: 1
default_conn_delay: 0.1
rejected_code: 429
key_type: var
key: remote_addr
policy: redis
redis_host: "redis-standalone.aic.svc"
redis_password: redis-password
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: limit-conn-redis
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /limit-conn/redis
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: limit-conn-redis-plugin-config
backendRefs:
- name: httpbin-external-domain
port: 80apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: httpbin-external-domain
spec:
ingressClassName: apisix
externalNodes:
- type: Domain
name: httpbin.org
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: limit-conn-redis
spec:
ingressClassName: apisix
http:
- name: limit-conn-redis
match:
paths:
- /limit-conn/redis
methods:
- GET
upstreams:
- name: httpbin-external-domain
plugins:
- name: proxy-rewrite
config:
uri: /delay/2
- name: limit-conn
config:
conn: 1
burst: 1
default_conn_delay: 0.1
rejected_code: 429
key_type: var
key: remote_addr
policy: redis
redis_host: "redis-standalone.aic.svc"
redis_password: redis-passwordApply the configuration:
kubectl apply -f limit-conn-ic.yaml❶ policy: Set to redis to store connection counters in Redis.
❷ redis_host: Set to the Redis hostname reachable from the gateway.
❸ redis_password: Set to the password configured for Redis.
Send five concurrent requests to the route:
seq 1 5 | xargs -n1 -P5 bash -c 'curl -s -o /dev/null -w "Response: %{http_code}\n" "http://127.0.0.1:9080/limit-conn/redis"'You should see responses similar to the following, where excessive requests are rejected:
Response: 200
Response: 200
Response: 429
Response: 429
Response: 429The exact order can vary, but no more than two requests should be admitted at once: one within conn and one within burst. Send requests through different nodes in the same gateway cluster or gateway group to verify that Redis enforces the limit across nodes rather than once per node.
Share Quota Among Gateway Nodes with a Redis Cluster
This example stores connection counters in a Redis Cluster so multiple gateway nodes share a partitioned, replicated counter store.
Start Redis Cluster
Set GATEWAY_CONTAINER to the running APISIX or API7 Gateway container. Create a dedicated network and connect the gateway to it:
export GATEWAY_CONTAINER=replace-with-gateway-container-name
docker network create gateway-redis-cluster-net
docker network connect gateway-redis-cluster-net "$GATEWAY_CONTAINER"Repeat the network connection for every additional gateway container used to verify the shared counter:
docker network connect gateway-redis-cluster-net replace-with-additional-gateway-containerCreate the following Docker Compose file for three primary nodes and three replicas:
x-redis-node: &redis-node
image: redis:8.10.2-alpine
command:
- redis-server
- --cluster-enabled
- "yes"
- --cluster-config-file
- nodes.conf
- --cluster-node-timeout
- "5000"
- --appendonly
- "no"
- --requirepass
- redis-cluster-password
- --masterauth
- redis-cluster-password
healthcheck:
test: ["CMD", "redis-cli", "-a", "redis-cluster-password", "ping"]
interval: 2s
timeout: 2s
retries: 30
networks:
- gateway-redis-cluster-net
services:
redis-cluster-1:
<<: *redis-node
container_name: redis-cluster-1
redis-cluster-2:
<<: *redis-node
container_name: redis-cluster-2
redis-cluster-3:
<<: *redis-node
container_name: redis-cluster-3
redis-cluster-4:
<<: *redis-node
container_name: redis-cluster-4
redis-cluster-5:
<<: *redis-node
container_name: redis-cluster-5
redis-cluster-6:
<<: *redis-node
container_name: redis-cluster-6
networks:
gateway-redis-cluster-net:
external: trueStart the nodes and wait for their health checks:
docker compose up -d --waitInitialize the cluster with one replica for each primary:
docker exec -e REDISCLI_AUTH=redis-cluster-password redis-cluster-1 \
redis-cli --cluster create \
redis-cluster-1:6379 \
redis-cluster-2:6379 \
redis-cluster-3:6379 \
redis-cluster-4:6379 \
redis-cluster-5:6379 \
redis-cluster-6:6379 \
--cluster-replicas 1 \
--cluster-yesThe output should end with [OK] All 16384 slots covered.
Wait a few seconds for the nodes to exchange cluster state, then verify that all hash slots are available:
docker exec -e REDISCLI_AUTH=redis-cluster-password redis-cluster-1 \
redis-cli cluster infoThe output should include cluster_state:ok and cluster_slots_assigned:16384.
Set the node host names used by the Admin API and ADC examples:
export REDIS_CLUSTER_NODE_1=redis-cluster-1
export REDIS_CLUSTER_NODE_2=redis-cluster-2
export REDIS_CLUSTER_NODE_3=redis-cluster-3
export REDIS_CLUSTER_NODE_4=redis-cluster-4
export REDIS_CLUSTER_NODE_5=redis-cluster-5
export REDIS_CLUSTER_NODE_6=redis-cluster-6Create a six-node Redis Cluster StatefulSet and headless Service:
apiVersion: apps/v1
kind: StatefulSet
metadata:
namespace: aic
name: redis-cluster
spec:
serviceName: redis-cluster
replicas: 6
selector:
matchLabels:
app: redis-cluster
template:
metadata:
labels:
app: redis-cluster
spec:
containers:
- name: redis
image: redis:8.10.2-alpine
ports:
- containerPort: 6379
name: client
- containerPort: 16379
name: gossip
command:
- redis-server
- --cluster-enabled
- "yes"
- --cluster-config-file
- nodes.conf
- --cluster-node-timeout
- "5000"
- --appendonly
- "yes"
- --requirepass
- redis-cluster-password
- --masterauth
- redis-cluster-password
volumeMounts:
- name: data
mountPath: /data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: redis-cluster
spec:
clusterIP: None
selector:
app: redis-cluster
ports:
- port: 6379
name: client
- port: 16379
name: gossipApply the manifest and wait for all nodes to be ready:
kubectl apply -f redis-cluster.yaml
kubectl rollout status statefulset/redis-cluster -n aicInitialize the cluster with one replica for each primary:
kubectl exec -n aic redis-cluster-0 \
-- env REDISCLI_AUTH=redis-cluster-password redis-cli \
--cluster create \
redis-cluster-0.redis-cluster.aic.svc:6379 \
redis-cluster-1.redis-cluster.aic.svc:6379 \
redis-cluster-2.redis-cluster.aic.svc:6379 \
redis-cluster-3.redis-cluster.aic.svc:6379 \
redis-cluster-4.redis-cluster.aic.svc:6379 \
redis-cluster-5.redis-cluster.aic.svc:6379 \
--cluster-replicas 1 \
--cluster-yesVerify that the cluster is ready:
kubectl exec -n aic redis-cluster-0 \
-- env REDISCLI_AUTH=redis-cluster-password redis-cli cluster infoThe output should include cluster_state:ok and cluster_slots_assigned:16384.
Set the node host names used by the Admin API and ADC examples:
export REDIS_CLUSTER_NODE_1=redis-cluster-0.redis-cluster.aic.svc
export REDIS_CLUSTER_NODE_2=redis-cluster-1.redis-cluster.aic.svc
export REDIS_CLUSTER_NODE_3=redis-cluster-2.redis-cluster.aic.svc
export REDIS_CLUSTER_NODE_4=redis-cluster-3.redis-cluster.aic.svc
export REDIS_CLUSTER_NODE_5=redis-cluster-4.redis-cluster.aic.svc
export REDIS_CLUSTER_NODE_6=redis-cluster-5.redis-cluster.aic.svcThese fixtures are for evaluation. For production, follow Redis guidance for persistence, authentication, TLS, failure recovery, and topology sizing.
Create a Route
Create the route in the gateway cluster or gateway group. Each node must load the same route resource and be able to reach the Redis Cluster. The route sends requests to httpbin's two-second delay endpoint so connections overlap during verification.
curl "http://127.0.0.1:9180/apisix/admin/routes/limit-conn-redis-cluster" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d @- <<EOF
{
"uri": "/limit-conn/redis-cluster",
"plugins": {
"proxy-rewrite": {
"uri": "/delay/2"
},
"limit-conn": {
"conn": 1,
"burst": 1,
"default_conn_delay": 0.1,
"rejected_code": 429,
"key_type": "var",
"key": "remote_addr",
"policy": "redis-cluster",
"redis_cluster_nodes": [
"$REDIS_CLUSTER_NODE_1:6379",
"$REDIS_CLUSTER_NODE_2:6379",
"$REDIS_CLUSTER_NODE_3:6379",
"$REDIS_CLUSTER_NODE_4:6379",
"$REDIS_CLUSTER_NODE_5:6379",
"$REDIS_CLUSTER_NODE_6:6379"
],
"redis_password": "redis-cluster-password",
"redis_cluster_name": "limit-conn-docs-redis-cluster"
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}
EOFservices:
- name: limit-conn-redis-cluster-service
labels:
docs-example: limit-conn-redis-cluster
routes:
- name: limit-conn-redis-cluster
uris:
- /limit-conn/redis-cluster
plugins:
proxy-rewrite:
uri: /delay/2
limit-conn:
conn: 1
burst: 1
default_conn_delay: 0.1
rejected_code: 429
key_type: var
key: remote_addr
policy: redis-cluster
redis_cluster_nodes:
- "${REDIS_CLUSTER_NODE_1}:6379"
- "${REDIS_CLUSTER_NODE_2}:6379"
- "${REDIS_CLUSTER_NODE_3}:6379"
- "${REDIS_CLUSTER_NODE_4}:6379"
- "${REDIS_CLUSTER_NODE_5}:6379"
- "${REDIS_CLUSTER_NODE_6}:6379"
redis_password: redis-cluster-password
redis_cluster_name: limit-conn-docs-redis-cluster
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1Preview changes owned by this example and confirm that the diff contains no unintended updates or deletions:
adc diff -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=limit-conn-redis-clusterSynchronize the reviewed service configuration:
adc sync -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=limit-conn-redis-clusterapiVersion: v1
kind: Service
metadata:
namespace: aic
name: httpbin-external-domain
spec:
type: ExternalName
externalName: httpbin.org
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: limit-conn-plugin-config
spec:
plugins:
- name: limit-conn
config:
conn: 1
burst: 1
default_conn_delay: 0.1
rejected_code: 429
key_type: var
key: remote_addr
policy: redis-cluster
redis_cluster_nodes:
- "redis-cluster-0.redis-cluster.aic.svc:6379"
- "redis-cluster-1.redis-cluster.aic.svc:6379"
- "redis-cluster-2.redis-cluster.aic.svc:6379"
- "redis-cluster-3.redis-cluster.aic.svc:6379"
- "redis-cluster-4.redis-cluster.aic.svc:6379"
- "redis-cluster-5.redis-cluster.aic.svc:6379"
redis_password: redis-cluster-password
redis_cluster_name: limit-conn-docs-redis-cluster
- name: proxy-rewrite
config:
uri: /delay/2
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: limit-conn-redis-cluster
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /limit-conn/redis-cluster
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: limit-conn-plugin-config
backendRefs:
- name: httpbin-external-domain
port: 80apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: httpbin-external-domain
spec:
ingressClassName: apisix
externalNodes:
- type: Domain
name: httpbin.org
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: limit-conn-redis-cluster
spec:
ingressClassName: apisix
http:
- name: limit-conn-redis-cluster
match:
paths:
- /limit-conn/redis-cluster
methods:
- GET
upstreams:
- name: httpbin-external-domain
plugins:
- name: limit-conn
config:
conn: 1
burst: 1
default_conn_delay: 0.1
rejected_code: 429
key_type: var
key: remote_addr
policy: redis-cluster
redis_cluster_nodes:
- "redis-cluster-0.redis-cluster.aic.svc:6379"
- "redis-cluster-1.redis-cluster.aic.svc:6379"
- "redis-cluster-2.redis-cluster.aic.svc:6379"
- "redis-cluster-3.redis-cluster.aic.svc:6379"
- "redis-cluster-4.redis-cluster.aic.svc:6379"
- "redis-cluster-5.redis-cluster.aic.svc:6379"
redis_password: redis-cluster-password
redis_cluster_name: limit-conn-docs-redis-cluster
- name: proxy-rewrite
config:
uri: /delay/2Apply the configuration:
kubectl apply -f limit-conn-ic.yaml❶ policy: Set to redis-cluster to store connection counters in a Redis Cluster.
❷ redis_cluster_nodes: Set to node addresses that the gateway can resolve and reach. The client discovers the rest of the cluster from these seed nodes.
❸ redis_password: Set to the password configured for the Redis Cluster.
❹ redis_cluster_name: Set to a name that identifies the Redis Cluster in the gateway.
Send five concurrent requests to the route:
seq 1 5 | xargs -n1 -P5 bash -c 'curl -s -o /dev/null -w "Response: %{http_code}\n" "http://127.0.0.1:9080/limit-conn/redis-cluster"'You should see responses similar to the following, where excessive requests are rejected:
Response: 200
Response: 200
Response: 429
Response: 429
Response: 429The exact order can vary, but no more than two requests should be admitted at once. Send requests through different nodes in the same gateway cluster or gateway group to verify that the cluster-backed counter is shared.
Rate Limit by Rules
limit-conn can apply different rate-limiting rules based on request attributes. This capability was introduced in API7 Enterprise 3.8.17 and APISIX 3.16.0. The following example uses HTTP header values to represent the caller's access tier.
Rules are applied sequentially. If a configured key does not exist, the corresponding rule is skipped.
tip
Rules can also use other built-in variables (opens in API7 Gateway docs) to implement rate limits for different request attributes.
Create a route that applies one limit per subscription and a stricter limit for trial users. The example reads subscription and trial identifiers from request headers:
curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "limit-conn-rules-route",
"uri": "/get",
"plugins": {
"limit-conn": {
"rejected_code": 429,
"default_conn_delay": 0.1,
"policy": "local",
"rules": [
{
"key": "${http_x_subscription_id}",
"conn": "${http_x_custom_conn ?? 5}",
"burst": 1
},
{
"key": "${http_x_trial_id}",
"conn": 1,
"burst": 1
}
]
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'services:
- name: httpbin
labels:
docs-example: limit-conn-rules
routes:
- uris:
- /get
name: limit-conn-rules-route
plugins:
limit-conn:
rejected_code: 429
default_conn_delay: 0.1
policy: local
rules:
- key: "${http_x_subscription_id}"
conn: "${http_x_custom_conn ?? 5}"
burst: 1
- key: "${http_x_trial_id}"
conn: 1
burst: 1
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1Preview changes owned by this example and confirm that the diff contains no unintended updates or deletions:
adc diff -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=limit-conn-rulesSynchronize the reviewed service configuration:
adc sync -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=limit-conn-rulesapiVersion: v1
kind: Service
metadata:
namespace: aic
name: httpbin-external-domain
spec:
type: ExternalName
externalName: httpbin.org
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: limit-conn-plugin-config
spec:
plugins:
- name: limit-conn
config:
rejected_code: 429
default_conn_delay: 0.1
policy: local
rules:
- key: "${http_x_subscription_id}"
conn: "${http_x_custom_conn ?? 5}"
burst: 1
- key: "${http_x_trial_id}"
conn: 1
burst: 1
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: limit-conn-rules-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /get
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: limit-conn-plugin-config
backendRefs:
- name: httpbin-external-domain
port: 80apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: httpbin-external-domain
spec:
ingressClassName: apisix
externalNodes:
- type: Domain
name: httpbin.org
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: limit-conn-rules-route
spec:
ingressClassName: apisix
http:
- name: limit-conn-rules-route
match:
paths:
- /get
methods:
- GET
upstreams:
- name: httpbin-external-domain
plugins:
- name: limit-conn
config:
rejected_code: 429
default_conn_delay: 0.1
policy: local
rules:
- key: "${http_x_subscription_id}"
conn: "${http_x_custom_conn ?? 5}"
burst: 1
- key: "${http_x_trial_id}"
conn: 1
burst: 1Apply the configuration:
kubectl apply -f limit-conn-ic.yaml❶ Use the value of the X-Subscription-ID request header as the rate-limiting key.
❷ Set the request connection dynamically based on the X-Custom-Conn header. If the header is not provided, a default concurrent connection count of 5 is applied.
❸ Use the value of the X-Trial-ID request header as the rate-limiting key.
To verify rate limiting, send 7 concurrent requests to the route with the same subscription ID:
seq 1 7 | xargs -n1 -P7 bash -c 'curl -s -o /dev/null -w "Response: %{http_code}\n" "http://127.0.0.1:9080/get" -H "X-Subscription-ID: sub-123456789"'You should see the following response, which shows that the default concurrent connection limit of 5 with a burst of 1 is applied when the X-Custom-Conn header is not provided:
Response: 429
Response: 200
Response: 200
Response: 200
Response: 200
Response: 200
Response: 200Send 5 concurrent requests to the route with the same subscription ID and set the X-Custom-Conn header to 1:
seq 1 5 | xargs -n1 -P5 bash -c 'curl -s -o /dev/null -w "Response: %{http_code}\n" "http://127.0.0.1:9080/get" -H "X-Subscription-ID: sub-123456789" -H "X-Custom-Conn: 1"'You should see the following response, which shows that the concurrent connection limit of 1 with a burst of 1 is applied:
Response: 429
Response: 429
Response: 429
Response: 200
Response: 200Finally, generate 5 requests to the route with the trial ID header:
seq 1 5 | xargs -n1 -P5 bash -c 'curl -s -o /dev/null -w "Response: %{http_code}\n" "http://127.0.0.1:9080/get" -H "X-Trial-ID: trial-123456789"'You should see the following response, which shows that the concurrent connection limit of 1 with a burst of 1 is applied:
Response: 429
Response: 429
Response: 429
Response: 200
Response: 200