Docs
Apache APISIXManage traffic and APIsTraffic ManagementManage Traffic Conditionally

Manage Traffic Conditionally

Match requests in Apache APISIX, then split traffic, label requests, or apply a rate limit.

Applies to:Apache APISIX 3.18.0

Roles:Platform engineerTime:20 minutesImpact:No restart, no traffic change

Goal

Three isolated routes that apply traffic behavior only when a request matches a condition:

  • traffic-split sends matching requests between two upstreams by weight.
  • traffic-label selects a weighted request-header action.
  • workflow applies a one-request-per-minute limit only to one query-selected flow.

Before you begin

  • You run Apache APISIX 3.18.0 or later. This includes the traffic-label plugin introduced in APISIX 3.17.0.
  • You have cURL and jq. Get APISIX starts an instance with Docker.
  • Route IDs conditional-split, conditional-label, and conditional-limit, and the matching /conditional/ paths, are unused.

Set the URLs and read the Admin API key:

export APISIX_ADMIN_URL="http://127.0.0.1:9180"
export GATEWAY_URL="http://127.0.0.1:9080"
read -rsp "Admin API key: " APISIX_ADMIN_KEY && export APISIX_ADMIN_KEY && echo

Check the three route IDs and paths before creating anything:

for route_id in conditional-split conditional-label conditional-limit; do
  curl -s -o /dev/null -w "${route_id}: %{http_code}\n" \
    "${APISIX_ADMIN_URL}/apisix/admin/routes/${route_id}" \
    -H "X-API-KEY: ${APISIX_ADMIN_KEY}"
done

curl -s "${APISIX_ADMIN_URL}/apisix/admin/routes" \
  -H "X-API-KEY: ${APISIX_ADMIN_KEY}" | \
  jq -r '.list[] | select(
    .value.uri == "/conditional/split" or
    .value.uri == "/conditional/label" or
    .value.uri == "/conditional/limit"
  ) | .key'

Expected: each ID lookup returns 404, and the path lookup has no output. Stop and choose isolated names if either check finds an existing route.

Steps

Create and verify each conditional behavior separately.

Step 1: Split matching traffic by weight

Create a route whose traffic-split rule runs only when name=jack. The first weighted entry sends traffic to httpbin; the entry without an upstream uses the route's default mock.api7.ai upstream.

curl -s "${APISIX_ADMIN_URL}/apisix/admin/routes/conditional-split" -X PUT \
  -H "X-API-KEY: ${APISIX_ADMIN_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "uri": "/conditional/split",
    "plugins": {
      "traffic-split": {
        "rules": [{
          "match": [{"vars": [["arg_name", "==", "jack"]]}],
          "weighted_upstreams": [
            {
              "upstream": {
                "type": "roundrobin",
                "scheme": "https",
                "pass_host": "node",
                "nodes": {"httpbin.org:443": 1}
              },
              "weight": 3
            },
            {"weight": 2}
          ]
        }]
      }
    },
    "upstream": {
      "type": "roundrobin",
      "scheme": "https",
      "pass_host": "node",
      "nodes": {"mock.api7.ai:443": 1}
    }
  }'

Expected: the route response contains one matching rule and weights 3 and 2.

What changed: only requests with name=jack are eligible for the 60:40 split; other requests use the default upstream.

Step 2: Apply a weighted request label

Create a second route that adds one of two headers or applies no header. An action containing only a weight performs no change after this final rule.

curl -s "${APISIX_ADMIN_URL}/apisix/admin/routes/conditional-label" -X PUT \
  -H "X-API-KEY: ${APISIX_ADMIN_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "uri": "/conditional/label",
    "plugins": {
      "traffic-label": {
        "rules": [{
          "match": [["uri", "==", "/conditional/label"]],
          "actions": [
            {"set_headers": {"X-Server-Id": "100"}, "weight": 3},
            {"set_headers": {"X-API-Version": "v2"}, "weight": 2},
            {"weight": 5}
          ]
        }]
      },
      "proxy-rewrite": {"uri": "/anything/conditional-label"}
    },
    "upstream": {
      "type": "roundrobin",
      "nodes": {"httpbin.org:80": 1}
    }
  }'

Expected: the route response contains action weights 3, 2, and 5.

What changed: every request matches the rule, then one weighted action labels the upstream request or deliberately leaves it unlabelled.

Step 3: Rate-limit one conditional flow

Use workflow to apply limit-count only when env=v1:

curl -s "${APISIX_ADMIN_URL}/apisix/admin/routes/conditional-limit" -X PUT \
  -H "X-API-KEY: ${APISIX_ADMIN_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "uri": "/conditional/limit",
    "plugins": {
      "workflow": {
        "rules": [{
          "case": [["arg_env", "==", "v1"]],
          "actions": [[
            "limit-count",
            {"count": 1, "time_window": 60, "rejected_code": 429}
          ]]
        }]
      },
      "proxy-rewrite": {"uri": "/anything/conditional-limit"}
    },
    "upstream": {
      "type": "roundrobin",
      "nodes": {"httpbin.org:80": 1}
    }
  }'

Expected: the route response contains one workflow rule whose only action is limit-count.

What changed: the quota applies to env=v1; requests with another value bypass this workflow action.

Validate

Send 20 matching split requests and count which upstream answered:

responses=$(for _ in $(seq 20); do
  curl -s "${GATEWAY_URL}/conditional/split?name=jack"
done)
printf 'httpbin: %s, mock: %s\n' \
  "$(printf '%s' "${responses}" | grep -c 'httpbin.org')" \
  "$(printf '%s' "${responses}" | grep -c 'mock.api7.ai')"

Expected: both counts are nonzero and tend toward the configured 60:40 ratio as the sample grows. Do not require an exact split from one small sample. Without the condition, all five requests use the default upstream:

for _ in $(seq 5); do
  curl -s "${GATEWAY_URL}/conditional/split?name=other"
done | grep -c 'mock.api7.ai'

Expected: 5.

Sample the weighted labels and summarize the three outcomes:

for _ in $(seq 50); do
  curl -s "${GATEWAY_URL}/conditional/label" | \
    jq -r 'if .headers["X-Server-Id"] then "server-id"
      elif .headers["X-Api-Version"] then "api-version"
      else "unlabelled" end'
done | sort | uniq -c

Expected: all three labels appear. Over larger samples, their proportions tend toward 30%, 20%, and 50%; a 50-request sample need not equal those percentages exactly.

Send two matching requests, then two bypass requests:

for _ in 1 2; do
  curl -s -o /dev/null -w '%{http_code}\n' \
    "${GATEWAY_URL}/conditional/limit?env=v1"
done

for _ in 1 2; do
  curl -s -o /dev/null -w '%{http_code}\n' \
    "${GATEWAY_URL}/conditional/limit?env=v2"
done

Expected: the matching pair returns 200 then 429; both bypass requests return 200.

Roll back / Clean up

Delete only the three routes created by this guide:

for route_id in conditional-split conditional-label conditional-limit; do
  curl -s -X DELETE \
    "${APISIX_ADMIN_URL}/apisix/admin/routes/${route_id}" \
    -H "X-API-KEY: ${APISIX_ADMIN_KEY}"
done

unset APISIX_ADMIN_KEY

Expected: all three deletions succeed and the example paths no longer match these routes.

Troubleshoot

SymptomCauseFix
Split requests always use the default upstream.The condition does not match or the override upstream is unreachable.Send name=jack, then inspect both upstreams directly from the APISIX network.
A label count is not exactly 30%, 20%, or 50%.Weights describe selection proportions, not an exact promise for every finite sample.Increase the sample size and validate that every expected outcome appears; do not use exact small-sample counts as a health check.
The first matching rate-limit request returns 429.The local counter for your source address is still inside its 60-second window.Wait for the current window to expire or use a unique key strategy when adapting the example.
Bypass requests are also rate-limited.A route-, service-, or global-rule rate-limit plugin applies independently of this workflow.Inspect the effective route, service, and global-rule configuration before attributing the response to this rule.