This is an automated email from the ASF dual-hosted git repository.
moonming pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix.git
The following commit(s) were added to refs/heads/master by this push:
new a4b8cf0a25 docs: add stream proxy example for limit-conn plugin
(#13936)
a4b8cf0a25 is described below
commit a4b8cf0a2583ff9d4f2794d7f5e00896242cee27
Author: Bhuvan Somisetty <[email protected]>
AuthorDate: Tue Nov 10 11:58:13 2026 +0530
docs: add stream proxy example for limit-conn plugin (#13936)
---
docs/en/latest/plugins/limit-conn.md | 308 +++++++++++++++++++++++++++++++++++
t/stream-plugin/limit-conn.t | 53 ++++++
2 files changed, 361 insertions(+)
diff --git a/docs/en/latest/plugins/limit-conn.md
b/docs/en/latest/plugins/limit-conn.md
index 56902f13f1..ce57b61995 100644
--- a/docs/en/latest/plugins/limit-conn.md
+++ b/docs/en/latest/plugins/limit-conn.md
@@ -1568,3 +1568,311 @@ Response: 429
Response: 200
Response: 200
```
+
+### Apply Rate Limiting in Stream Proxy
+
+The `limit-conn` Plugin can also be used on a [stream
Route](../stream-proxy.md) to limit the number of concurrent TCP connections.
+
+The following example demonstrates how to rate limit TCP connections by
`remote_addr`, with example connection and burst thresholds.
+
+:::note
+
+When `key_type` is `var` (the default), `key` is resolved as an [NGINX stream
module variable](https://nginx.org/en/docs/stream/ngx_stream_core_module.html).
It is not limited to `remote_addr` or `server_addr`: any variable available in
the stream context, such as `server_port`, can be used.
+
+:::
+
+The stream `limit-conn` Plugin only accepts the following attributes.
HTTP-only attributes, such as `policy`, `rejected_code`, and the Redis options,
are not applicable to stream Routes:
+
+| Name | Type | Required | Default | Valid values | Description |
+|------|------|----------|---------|--------------|-------------|
+| conn | integer | True | | > 0 | The maximum number of concurrent connections
allowed. Connections exceeding the configured limit and at or below `conn +
burst` will be delayed. |
+| burst | integer | True | | >= 0 | The number of excessive concurrent
connections allowed to be delayed. Connections exceeding `conn + burst` will be
rejected immediately. |
+| default_conn_delay | number | True | | > 0 | Processing latency allowed in
seconds for concurrent connections exceeding `conn` and up to `conn + burst`. |
+| only_use_default_delay | boolean | False | false | | Has no effect on stream
Routes. The proportional-delay behavior this setting toggles for HTTP Routes
relies on measuring request latency, which the stream subsystem does not
compute, so excess connections are always delayed by `default_conn_delay ×
floor((current connections - 1) / conn)`. |
+| key_type | string | False | var | [`var`, `var_combination`] | The type of
key. If `key_type` is `var`, `key` is interpreted as a variable. If `key_type`
is `var_combination`, `key` is interpreted as a combination of variables. |
+| key | string | True | | | The key to count connections by. If the configured
key resolves to an empty value, APISIX falls back to `remote_addr`. |
+
+Before creating the Route, make sure APISIX is listening for stream traffic on
port `9100`. Add the following to `conf/config.yaml` and reload APISIX for the
changes to take effect:
+
+```yaml title="conf/config.yaml"
+apisix:
+ proxy_mode: http&stream # Enable both L4 & L7 proxies
+ stream_proxy: # Configure L4 proxy
+ tcp:
+ - 9100 # Set TCP proxy listening port
+```
+
+You also need a TCP service listening on `127.0.0.1:1995` to act as the
upstream. If you do not already have one, you can start a simple TCP echo
service with [`socat`](http://www.dest-unreach.org/socat/):
+
+```shell
+socat -v TCP-LISTEN:1995,fork,reuseaddr EXEC:'/bin/cat'
+```
+
+Leave this running in its own terminal for the rest of the example.
+
+:::note
+
+This assumes APISIX is running directly on the host, where `127.0.0.1` is
shared between APISIX and `socat`. If APISIX is running in a container,
`127.0.0.1:1995` inside that container refers to the container itself, not the
host running `socat`. Either run `socat` in the same container/network
namespace as APISIX, or replace `127.0.0.1` in the upstream node with an
address the container can reach, such as `host.docker.internal` (Docker
Desktop) or the host's container-network gateway address.
+
+:::
+
+Create a stream Route with `limit-conn` Plugin as such:
+
+<Tabs groupId="api">
+<TabItem value="admin-api" label="Admin API">
+
+```shell
+curl "http://127.0.0.1:9180/apisix/admin/stream_routes" -X PUT \
+ -H "X-API-KEY: ${admin_key}" \
+ -d '{
+ "id": "limit-conn-stream-route",
+ "plugins": {
+ "limit-conn": {
+ "conn": 1,
+ "burst": 0,
+ "default_conn_delay": 0.1,
+ "key_type": "var",
+ "key": "remote_addr"
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:1995": 1
+ }
+ }
+ }'
+```
+
+</TabItem>
+<TabItem value="adc" label="ADC">
+
+```yaml title="adc.yaml"
+services:
+ - name: tcp-echo-service
+ upstream:
+ name: default
+ scheme: tcp
+ nodes:
+ - host: 127.0.0.1
+ port: 1995
+ weight: 1
+ stream_routes:
+ - name: limit-conn-stream-route
+ server_port: 9100
+ plugins:
+ limit-conn:
+ conn: 1
+ burst: 0
+ default_conn_delay: 0.1
+ key_type: var
+ key: remote_addr
+```
+
+Synchronize the configuration to the gateway:
+
+```shell
+adc sync -f adc.yaml
+```
+
+</TabItem>
+<TabItem value="ingress" label="Ingress Controller">
+
+<Tabs groupId="k8s-api">
+<TabItem value="gateway-api" label="Gateway API">
+
+:::note
+
+Attaching stream Plugins through Gateway API requires [APISIX Ingress
Controller](https://github.com/apache/apisix-ingress-controller) 2.2.0 or
later, which introduced `L4RoutePolicy` for attaching stream Plugins to
`TCPRoute`, `UDPRoute`, and `TLSRoute`. If you are on an earlier controller
version, use the APISIX Ingress Controller tab instead.
+
+:::
+
+This example assumes a `Gateway` named `apisix` with a TCP listener on port
`9100` already exists, and deploys a `tcp-echo` Service as the upstream:
+
+```yaml title="limit-conn-stream-ic.yaml"
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ namespace: aic
+ name: tcp-echo
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: tcp-echo
+ template:
+ metadata:
+ labels:
+ app: tcp-echo
+ spec:
+ containers:
+ - name: tcp-echo
+ image: alpine/socat
+ args:
+ - "-v"
+ - "TCP-LISTEN:1995,fork,reuseaddr"
+ - "EXEC:/bin/cat"
+ ports:
+ - containerPort: 1995
+---
+apiVersion: v1
+kind: Service
+metadata:
+ namespace: aic
+ name: tcp-echo-service
+spec:
+ selector:
+ app: tcp-echo
+ ports:
+ - name: tcp-echo
+ port: 1995
+ targetPort: 1995
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: TCPRoute
+metadata:
+ namespace: aic
+ name: limit-conn-stream-route
+spec:
+ parentRefs:
+ - name: apisix
+ rules:
+ - backendRefs:
+ - name: tcp-echo-service
+ port: 1995
+---
+apiVersion: apisix.apache.org/v1alpha1
+kind: L4RoutePolicy
+metadata:
+ namespace: aic
+ name: limit-conn-l4-policy
+spec:
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: TCPRoute
+ name: limit-conn-stream-route
+ plugins:
+ - name: limit-conn
+ config:
+ conn: 1
+ burst: 0
+ default_conn_delay: 0.1
+ key_type: var
+ key: remote_addr
+```
+
+Apply the configuration:
+
+```shell
+kubectl apply -f limit-conn-stream-ic.yaml
+```
+
+</TabItem>
+<TabItem value="ingress" label="APISIX Ingress Controller">
+
+```yaml title="limit-conn-stream-ic.yaml"
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ namespace: aic
+ name: tcp-echo
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: tcp-echo
+ template:
+ metadata:
+ labels:
+ app: tcp-echo
+ spec:
+ containers:
+ - name: tcp-echo
+ image: alpine/socat
+ args:
+ - "-v"
+ - "TCP-LISTEN:1995,fork,reuseaddr"
+ - "EXEC:/bin/cat"
+ ports:
+ - containerPort: 1995
+---
+apiVersion: v1
+kind: Service
+metadata:
+ namespace: aic
+ name: tcp-echo-service
+spec:
+ selector:
+ app: tcp-echo
+ ports:
+ - name: tcp-echo
+ port: 1995
+ targetPort: 1995
+---
+apiVersion: apisix.apache.org/v2
+kind: ApisixRoute
+metadata:
+ namespace: aic
+ name: limit-conn-stream-route
+spec:
+ ingressClassName: apisix
+ stream:
+ - name: limit-conn-stream-route
+ protocol: TCP
+ match:
+ ingressPort: 9100
+ backend:
+ serviceName: tcp-echo-service
+ servicePort: 1995
+ plugins:
+ - name: limit-conn
+ enable: true
+ config:
+ conn: 1
+ burst: 0
+ default_conn_delay: 0.1
+ key_type: var
+ key: remote_addr
+```
+
+Apply the configuration:
+
+```shell
+kubectl apply -f limit-conn-stream-ic.yaml
+```
+
+</TabItem>
+</Tabs>
+
+</TabItem>
+</Tabs>
+
+`key_type` is set to `var` so that `key` is interpreted as a variable, and
`key` is set to `remote_addr` so the rate-limiting count is calculated by the
connection's `remote_addr`.
+
+With `conn` set to `1` and `burst` set to `0`, only one concurrent connection
from the same `remote_addr` is allowed, and a second concurrent connection is
rejected immediately. Open one long-lived connection in the background, using
`sleep` as the input source so the connection stays open regardless of terminal
job control, and save its PID:
+
+```shell
+sleep 30 | nc 127.0.0.1 9100 &
+first_pid=$!
+```
+
+While that connection is still open, attempt a second one:
+
+```shell
+nc -w 1 -v 127.0.0.1 9100
+```
+
+The second `nc` call is refused and its TCP connection is reset by APISIX,
which `nc -v` reports on stderr, similar to:
+
+```text
+nc: connect to 127.0.0.1 port 9100 (tcp) failed: Connection reset by peer
+```
+
+Now close the first connection and retry:
+
+```shell
+kill "$first_pid"
+nc -w 1 -v 127.0.0.1 9100
+```
+
+With the first connection gone, the second call connects successfully and
proxies to the upstream as expected.
diff --git a/t/stream-plugin/limit-conn.t b/t/stream-plugin/limit-conn.t
index 3a733475ef..cc1176cfaa 100644
--- a/t/stream-plugin/limit-conn.t
+++ b/t/stream-plugin/limit-conn.t
@@ -339,3 +339,56 @@ GET /test_concurrency
--- error_log
The value of the configured key is empty, use client IP instead
--- stream_enable
+
+
+
+=== TEST 11: key is a var other than remote_addr/server_addr
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/stream_routes/1',
+ ngx.HTTP_PUT,
+ [[{
+ "plugins": {
+ "limit-conn": {
+ "conn": 2,
+ "burst": 1,
+ "default_conn_delay": 0.1,
+ "key": "server_port"
+ }
+ },
+ "upstream_id": "1"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 12: exceeding the burst when keyed by a non-address var
+--- request
+GET /test_concurrency
+--- response_body
+200
+200
+200
+503
+503
+--- error_log eval
+[
+ qr/limit key: \/apisix\/stream_routes\/1:\d+:1985/,
+ qr/Connection reset by peer/,
+]
+--- no_error_log
+The value of the configured key is empty, use client IP instead
+--- stream_enable