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-splitsends matching requests between two upstreams by weight.traffic-labelselects a weighted request-header action.workflowapplies 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-labelplugin introduced in APISIX 3.17.0. - You have cURL and jq. Get APISIX starts an instance with Docker.
- Route IDs
conditional-split,conditional-label, andconditional-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 && echoCheck 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 -cExpected: 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"
doneExpected: 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_KEYExpected: all three deletions succeed and the example paths no longer match these routes.
Troubleshoot
| Symptom | Cause | Fix |
|---|---|---|
| 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. |
Related
traffic-splitplugin reference (opens in Plugin Hub docs) — conditional and weighted upstream selection.traffic-labelplugin reference (opens in Plugin Hub docs) — ordered rules and weighted header actions.workflowplugin reference (opens in Plugin Hub docs) — conditionalreturn,limit-count, andlimit-connactions.- APISIX Expressions — variables and operators used in request conditions.