Docs
Plugin HubOverview

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 policy is set or when policy is local.
  • 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
      }
    }
  }'

❶ 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: 429

Apply 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
      }
    }
  }'

❶ 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: 429

Immediately 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: 429

Matching 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.7

Set the upstream hostname used by the Admin API and ADC examples:

export WEBSOCKET_UPSTREAM_HOST=websocket-server

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
      }
    }
  }
EOF

Install 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
hello

Keep 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:

config.yaml
apisix:
  proxy_mode: http&stream
  stream_proxy:
    tcp:
      - 9100

For a host installation, reload APISIX after updating the configuration:

apisix reload

For 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 reload

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
      }
    }
  }'

❶ 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 9100

In 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 server

Depending 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-container

Start Redis on the shared network:

docker run -d \
  --name redis-standalone \
  --network gateway-redis-net \
  redis:8.10.2-alpine \
  redis-server --requirepass redis-password

Verify the authenticated connection:

docker exec -e REDISCLI_AUTH=redis-password redis-standalone redis-cli ping

You should receive PONG.

Set the Redis hostname used by the Admin API and ADC examples:

export REDIS_HOST=redis-standalone

These 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
      }
    }
  }
EOF

❶ 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: 429

The 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-container

Create the following Docker Compose file for three primary nodes and three replicas:

docker-compose.yml
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: true

Start the nodes and wait for their health checks:

docker compose up -d --wait

Initialize 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-yes

The 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 info

The 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-6

These 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
      }
    }
  }
EOF

❶ 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: 429

The 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
      }
    }
  }'

❶ 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: 200

Send 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: 200

Finally, 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