• English icon
  • German icon
Explainer

How to send 20% of traffic to a new version on Azure Container Apps

· Series: Containers on Azure

azureazure-container-appstraffic-splittingcanary-deploymentdevops

I wanted to test a new version of my app on twenty percent of the traffic. I switched my Azure Container App to multiple revisions, deployed the new version and sent 200 requests. The new version answered all 200.

This explainer shows how Container Apps decides which revision answers each request, how to get a real 80/20 split, what that split means for a single user, and what an old revision at zero percent was still doing. Everything below comes from a real run in my own Azure subscription.

Key points

  • Single revision mode can't split. It's the default: one active revision at a time, and a deploy is the release. Asking for weights fails with "configured for single revision".
  • Multiple revision mode doesn't hold a new version back. The default traffic rule is latestRevision: true at 100 %. It points at whatever is newest, so every deploy takes all the traffic once it's ready. Name the revisions in the weights before you deploy, and a new revision starts at zero.
  • 80/20 is per request, not per user. 1,000 requests split 806 / 194. One client on one keep-alive connection saw both versions. Sticky sessions only exist in single revision mode. For testers, a label gives one revision its own address.
  • Every active revision runs its own replicas. At 30 requests a second, one revision ran five replicas and two revisions at 50/50 ran two plus three. A revision at zero percent kept four replicas for four and a half minutes and then its minimum of one, until I deactivated it.

One revision at a time

Every change to the container (a new image, a new environment variable, a new scale rule: anything in the app's template) builds a new revision, a snapshot of the app that never changes. Each revision runs as replicas, copies of your container. Changes to the app's configuration (secrets, ingress, the traffic rules, labels) don't build a revision.

By default, a Container App runs in single revision mode: one revision is active at a time. When I deployed version two, v2 took all the traffic as soon as it was ready, and a little later v1 was deactivated on its own. In this mode, a deploy is the release.

So I asked for a split anyway, 80 % to v1 and 20 % to v2:

> az containerapp ingress traffic set -n shop -g rg-demo --revision-weight shop--v1=80 shop--v2=20
ERROR: Containerapp 'shop' is configured for single revision. Set revision mode to multiple in order to set ingress traffic.

One active revision means there's nothing to split.

The newest takes it all

Multiple revision mode lets several revisions be active at once, and you decide how they share the traffic. One command switches the mode:

az containerapp revision set-mode -n shop -g rg-demo --mode multiple

Then I deployed v3, my canary, the same way as before. v2 was still active next to it. 200 requests later, v3 had answered all 200. The traffic rules explained why:

> az containerapp ingress traffic show -n shop -g rg-demo
[
{
"latestRevision": true,
"weight": 100
}
]

That's the default. The rule doesn't name a revision; it points at whatever is newest. Multiple mode only kept v2 running next to the new one.

The fix is to name the revisions. Pin the weights by revision name first, then deploy, then move the weights yourself:

az containerapp ingress traffic set -n shop -g rg-demo \
--revision-weight shop--v2=100
az containerapp update -n shop -g rg-demo \
--set-env-vars APP_VERSION=v3 --revision-suffix v3
az containerapp ingress traffic set -n shop -g rg-demo \
--revision-weight shop--v2=80 shop--v3=20

Later in my run, with the weights still naming v2 and v3, I deployed v4: it became active with one replica and got 0 % of the traffic.

Single revision modeMultiple revision mode
Active revisionsoneas many as you keep active
A new deploytakes all traffic once ready; the old one is deactivateddefault rule: the latest takes 100 % once ready; named weights: starts at 0 %
Traffic splittingrefusedby weight (revision name or label)
Sticky sessionsavailablenot available
Old revisionsdeactivated for youkeep running until you deactivate them

The docs list a third mode, deployment labels (preview), where labels like stage and prod get new revisions assigned and old ones shut down when no label points at them.

Twenty percent of what?

With the weights at 80 and 20, I sent 1,000 requests, each on a new connection: 806 for v2, 194 for v3. Close to 80/20, not exact: each request is sent on its own, by chance, following the weights.

Then one user: one client, one keep-alive connection, ten requests in a row. It saw v2 v2 v2 v3 v2 v3 v2 v2 v2 v2. The split is per request, not per user. Session affinity (sticky sessions) is only supported in single revision mode, so if a user has to stay on one version, the weights alone won't do it.

For testers, there's a label. A label gives one revision its own address, whatever the weights say:

az containerapp revision label add -n shop -g rg-demo \
--label canary --revision shop--v3

The label's address is the app name, three dashes, the label, then the domain of the Container Apps environment: https://shop---canary.<env-domain>.westeurope.azurecontainerapps.io/. It answered v3 every time, 100 calls out of 100, while v3 still had 20 % of the normal traffic. A label can sit on a revision at 0 %, so you can test before you give it any real traffic. Real users follow the weights. Testers use the label.

Every gate has its own crew

Every active revision has its own replicas, and each one scales on its own share of the requests. My HTTP scale rule asked for about one replica per ten requests a second (--scale-rule-http-concurrency 10).

30 requests/s for 3 minutesv2v3Total
v3 at 100 %4 (left over, 0 % traffic)59
50 / 50 (after the scale-in)235

Split fifty-fifty, the same load used five replicas, the same as one revision alone. The split itself didn't cost extra replicas in my run; the module warns it can, because each revision keeps its own minimum and rounds up on its own. What adds up is every revision you leave active.

The old revision at zero percent

When I moved v2 to 0 %, it still had four replicas from the thousand requests before. It kept them for four and a half minutes, then two, then, eight minutes after it lost its traffic, one. And that one stays: it's the minimum replica count I gave the app, and every active revision keeps it. Zero percent of the traffic is not zero replicas.

At the end I deactivated the revisions I didn't need:

az containerapp revision deactivate -n shop -g rg-demo --revision shop--v4

An inactive revision runs no replicas and costs nothing (Container Apps keeps up to 100 of them, so you can activate one again).

Move the weight to zero before you deactivate. In my run, revision deactivate also succeeded on a revision that still had 50 % of the weight. The requests went to the other revision right away (200 of 200, also two minutes later), but the traffic list kept showing the inactive revision at 50 % until I set the weights again.

What happened when I ran it

Tested on October 21, 2026 in my own Azure subscription (westeurope, Azure CLI 2.88.0); everything was deleted afterwards. The app is my own tiny Python API that answers {"version": "v3", ...}; a new version is a new value of APP_VERSION plus a revision suffix. split.py, also my own, counted which version answered, played one user on one connection, sent load at a fixed rate and polled the replicas of every active revision. The environment took 17.5 minutes to create, each update about 20 seconds plus 15 seconds until the new revision was running.

After v3 in multiple mode, nothing pinned:

> az containerapp revision list -n shop -g rg-demo -o table
Active Replicas TrafficWeight Name
True 1 0 shop--v2
True 1 100 shop--v3
> python split.py --app shop --rg rg-demo send 200
200 requests to the app URL in 4 s
v3 200 100.0 %

The 80/20 split, 1,000 requests and one user:

> python split.py --app shop --rg rg-demo send 1000
1000 requests to the app URL in 18 s
v2 806 80.6 %
v3 194 19.4 %
> python split.py --app shop --rg rg-demo user 10
one client, one connection, 10 requests:
v2 v2 v2 v3 v2 v3 v2 v2 v2 v2
v2: 8, v3: 2

The label:

> curl.exe -s https://shop---canary.<env-domain>.westeurope.azurecontainerapps.io/
{"version": "v3", "replica": "?"}

v4 deployed while the weights named v2 and v3:

Active Replicas TrafficWeight Name
True 2 50 shop--v2
True 3 50 shop--v3
True 1 0 shop--v4

v2 at 0 % after the load test (my tool polled every 15 seconds):

2 s shop--v2 4 repl. (0 %) · shop--v3 5 repl. (100 %)
53 s shop--v2 2 repl. (0 %) · shop--v3 5 repl. (100 %)
277 s shop--v2 1 repl. (0 %) · shop--v3 4 repl. (100 %)
517 s shop--v2 1 repl. (0 %) · shop--v3 1 repl. (100 %)

The deactivation with weight left on it:

> az containerapp revision list -n shop -g rg-demo --all -o table
Active Replicas TrafficWeight Name
True 6 50 shop--v3
False 0 50 shop--v4

And one detail: right after containerapp create with a revision suffix, the list showed an extra, automatically named revision (shop--ttolvpb, active, one replica, 0 %) next to shop--v1; by the next deploy it was inactive.

Further reading

Video transcript

I wanted to test a new version of my app on twenty percent of the traffic: a canary. So I switched my Container App to multiple revisions and deployed it. Then I sent two hundred requests. The new version answered all two hundred. And the old one? Still running, with nobody to serve.

In this video, you'll see how Container Apps decides which revision answers each request, and how to get a real twenty percent. And at the end, what that old revision was still doing.

Why can't a normal deploy split traffic? Every change to the container, like a new image or a new environment variable, builds a new revision: a snapshot of your app that never changes. Mine are named shop, two dashes, v1, v2, and so on. Each revision runs as replicas, copies of your container. By default, a Container App runs in single revision mode: one revision is active at a time. I deployed version two of my app that way. As soon as v2 was ready, it took all the traffic, and a little later v1 was deactivated on its own: no replicas, no traffic. In this mode, a deploy is the release. So I asked for a split anyway: eighty percent to v1, twenty percent to v2. What did Azure do? It refused. I ran the traffic command, ingress traffic set, with a weight for each revision: eighty for v1, twenty for v2. Configured for single revision. Set the revision mode to multiple. One active revision means there's nothing to split. A split needs multiple revision mode.

So what changes in multiple revision mode? Several revisions can be active at once, and you decide how they share the traffic. One command switches the mode: revision set-mode, with the mode multiple, for my app, called shop. It answered: multiple. Then I deployed v3, my canary, the same way as before. v2 was still active next to it. How much of the traffic did v3 get? All of it. I sent two hundred requests to the app's address, the way users reach it, and counted which version answered each one. Version three, two hundred times. My canary was the release. To see why, I asked Container Apps for the app's traffic rules, the weights that decide who answers. One rule: the latest revision, one hundred percent. That's the default. The rule doesn't name a revision. It points at whatever is newest, so every new deploy takes all the traffic once it's ready. Multiple mode only kept v2 running next to it. The fix is to name the revisions. First, set the weights with the same traffic command as before, and name the revision: v2 gets one hundred percent. Then deploy the new version as usual, here a new value for the version variable, and the revision name v3. It starts, and gets nothing. Then you move the weights yourself, eighty for v2, twenty for v3. Much later in my run, with the weights still named, I deployed v4 and listed the app's active revisions. v4 was active, with one replica, and zero percent of the traffic. This series runs one Azure service at a time, for real. Subscribe to follow the series.

Twenty percent of what, exactly? With the weights at eighty and twenty, I sent a thousand requests, each one on a new connection, and counted the answers. Eight hundred and six for v2, one hundred and ninety-four for v3. Close to eighty-twenty, not exact. Each request is sent on its own, by chance, following the weights. Now one user, on one connection, sends ten requests in a row. Which versions does that user see? Both. Here's that run: the same tool as a single client that keeps its connection open. v2, v2, v2, v3, v2, v3. Eight times v2, twice v3. The split is per request, not per user. Sticky sessions, which keep a user on one replica, only exist in single revision mode. So if a user has to stay on one version, the weights alone won't do it. For testers, there's a label. A label gives one revision its own address, whatever the weights say. I put the label canary on v3 with one command: revision label add, with the label and the revision. The traffic list now shows the label next to v3, still at twenty percent. Then I called the label's address: the app name, three dashes, the label, then the domain of its environment, the shared space where Container Apps run. Version three. And a hundred more calls through the label: a hundred answers from v3. Real users follow the weights. Testers use the label.

Do two revisions cost more than one? Every active revision has its own replicas, and each one scales on its own share of the requests. My scale rule, the setting that decides when to add replicas, asked for about one replica per ten requests a second. First, v3 alone, with all the traffic. My tool sent thirty requests a second for three minutes and listed the replicas of every active revision. v3 went to five replicas. Then I split the same load fifty-fifty and ran the tool again. Two replicas for v2, three for v3. Five in total, the same as one revision alone. Each revision scaled on its own half. The split itself didn't cost extra replicas here. So what does cost extra?

To sum it up. In single revision mode, a deploy is the release, and there's nothing to split. In multiple mode, the default rule sends everything to the latest revision. Name your revisions in the weights before you deploy. The weights split requests, not users. Give testers a label. And every active revision runs its own replicas.

The old revision from the start. When I moved v2 to zero percent, it still had four replicas from the thousand requests before. After the load test, three and a half minutes later, my tool kept watching every fifteen seconds. Four replicas, then two, then one. Eight minutes in total after v2 lost its traffic. And that one stays. It's the minimum replica count I gave the app, one, and every active revision keeps it. Zero percent of the traffic is not zero replicas. At the end, I deactivated the revisions I didn't need, one command each: revision deactivate. Deactivate succeeded. An inactive revision runs no replicas and costs nothing. So: name the weights before you deploy, and deactivate the old revision when you're done.

In single mode, I watched v2 take over from v1. The video on screen now shows how a new revision takes over, and why it waits until it's ready. If this was useful, subscribe for more practical .NET and Azure.

Need this in your own Azure environment?

I build, migrate and run .NET platforms on Azure, with Azure DevOps and Terraform. If your team wants this set up properly, see what I offer.

See services