• English icon
  • German icon
Explainer

How does Azure Container Apps decide how many replicas to run?

· Series: Containers on Azure

azureazure-container-appsautoscalingcontainersdevops

I load-tested one container app twice with the same HTTP scale rule. Forty slow requests at a time got six replicas. Ten fast requests at a time got ten, the maximum. More requests waiting, fewer replicas. And when the traffic stopped, the last replica stayed for almost ten minutes.

This explainer shows what the HTTP scale rule in Azure Container Apps really counts, why replicas stay after the traffic is gone, and what scale to zero costs the first user. Everything below comes from a real run in my own Azure subscription.

Key points

  • Container Apps only scales between your minimum and maximum. The Microsoft Learn exercise starts the app with min 1 and max 1: forty users waited, nothing errored, and the replica count never moved.
  • The HTTP rule counts requests per second, not requests waiting. Every 15 seconds Container Apps takes the requests of the last 15 seconds and divides by 15. Fast requests produce more requests per second, so they get more replicas.
  • Replicas leave slowly. Extra replicas go after a 5-minute window; the last one has a cool down of its own (5 minutes by default). In my run, zero came 9 minutes 47 seconds after the last request.
  • Zero costs nothing, except the first user's wait. The first request after zero took 29.6 seconds. With a minimum of one it took 0.16 seconds, and an idle replica is billed at a lower rate.

One replica

A replica is one running copy of your container, and scaling out means more copies. Without a scale rule of your own, Container Apps uses a default HTTP rule between zero and ten replicas. The lab sets both limits to one, so there is no room to scale.

With forty users at once and two seconds of work per request, there was no error at all. The app's web server (gunicorn with 2 workers × 8 threads) handles sixteen requests at a time; the other twenty-four waited in line. Two seconds of work took a median of 4.15 seconds, and the replica count stayed at one for the whole 90 seconds.

The lab's HTTP rule:

az containerapp update -n agent-api -g rg-demo \
--min-replicas 0 --max-replicas 10 \
--scale-rule-name http-scaling \
--scale-rule-type http \
--scale-rule-http-concurrency 10

The same in the template:

scale:
minReplicas: 0
maxReplicas: 10
rules:
- name: http-scaling
http:
metadata:
concurrentRequests: "10"

Scale settings live in the revision's template, so this change builds a new revision (here agent-api--v2). A revision is a snapshot of your app's image and settings.

What the rule counts

The setting is called concurrentRequests, so it reads like "requests in flight, ten per replica". By that reading, forty slow clients need four replicas and ten fast clients need one. The run says otherwise:

TestClientsWork per requestRequests per secondReplicas
Slow402 sabout 206
Fast1050 msabout 12010 (the maximum)

The docs define the number as a rate: every 15 seconds, the requests of the last 15 seconds divided by 15. It counts customers coming through the door, not the line at the till. The fast clients came back quickly and sent about 120 requests per second: divided by the target of 10, that is 12 replicas, capped at the maximum of 10.

The slow clients sent only about 20 per second, a sixth of the fast ones, so they got fewer replicas. By the formula alone, 20 divided by 10 is two. I ran the slow test again for nine minutes, and it held six. The docs call replica counts a target, not a guarantee. The formula tells you the direction; load test with your real request times and watch the replica count.

How the scale-out moves, from the docs: desiredReplicas = ceil(current metric ÷ target), scale-up steps of 1, 4, 8, 16 … up to the maximum, no wait before a scale-up. In my slow test the count went 1 → 4 within 11 seconds, then 4 → 6.

When the traffic stops

PhaseDocs defaultMy run (slow test)
Extra replicas leaveafter a 300-second scale-down window6 → 1 at 329–345 s after the last request
The last replica → zeroafter the 300-second cool downat 587 s (9 min 47 s)
First request after zeroa cold start: new replica, image pull, app start29.6 s, then 0.14 s

While you wait for zero, every replica above the minimum is billed at the active rate. At zero replicas there are no usage charges.

Two surprises

A deployment during a peak. Right after the fast test I changed only the minimum (min 1, revision v3). The new revision started with ten replicas, as many as the old one had, and kept them for five more minutes. That is the zero-downtime switch: a new revision has to carry the current load before it gets the traffic. Don't read those replicas as a scaling bug.

No ingress and a minimum of zero. Without ingress there are no HTTP requests to count. If you also define no minimum and no other scale rule, the app scales to zero and has no way of starting again. Set a minimum of one, or scale on something else, like the length of a queue.

What happened when I ran it

Tested on October 20, 2026 in my own Azure subscription (westeurope, Azure CLI 2.88.0); everything was deleted afterwards. The app is the mock agent API from the Microsoft Learn exercise (Flask, POST /agent/process waits a given number of milliseconds); watch.py, my own script, sent the load from keep-alive clients and polled az containerapp replica list every few seconds. My first attempt in germanywestcentral failed at the environment, the same AKSCapacityHeavyUsage error as the day before. In westeurope the environment took 15.7 minutes, the cloud build 68 seconds, the app 41 seconds.

The lab's start, min 1 and max 1:

> az containerapp show -n agent-api -g rg-demo --query properties.template.scale -o json
{
"cooldownPeriod": 300,
"maxReplicas": 1,
"minReplicas": 1,
"pollingInterval": 30,
"rules": null
}

The fast test (10 clients × 50 ms), the first requests waiting for a cold start:

34 s replicas 1 | 48.7 req/s (last 15 s) 10 in flight median 0.08 s
42 s replicas 10 | 112.8 req/s (last 15 s) 10 in flight median 0.08 s
50 s replicas 10 | 120.3 req/s (last 15 s) 10 in flight median 0.08 s
summary: 11973 requests ok, 0 failed, median 0.08 s, p95 0.09 s, peak 10 replicas at 42 s

The slow test, nine minutes long:

16 s replicas 4 | 7.5 req/s (last 15 s) 40 in flight median 4.13 s
56 s replicas 6 | 20.9 req/s (last 15 s) 40 in flight median 2.03 s
302 s replicas 6 | 18.7 req/s (last 15 s) 40 in flight median 2.03 s
535 s replicas 6 | 20.9 req/s (last 15 s) 40 in flight median 2.03 s
summary: 10302 requests ok, 0 failed, median 2.03 s, p95 2.05 s, peak 6 replicas at 56 s

Scale to zero and the cold start:

126 s load stopped
456 s replicas 2
471 s replicas 1
713 s replicas 0
request 1 : HTTP 200 in 29.640707 s
request 2 : HTTP 200 in 0.137173 s
request 3 : HTTP 200 in 0.133291 s

With a minimum of one, after six idle minutes:

request 1 : HTTP 200 in 0.162562 s
request 2 : HTTP 200 in 0.107679 s
request 3 : HTTP 200 in 0.101485 s

Two details: az containerapp update printed "concurrentRequests": "" in its response, while revision show afterwards showed the stored value "10", so check the revision, not the update's output. And the Azure Monitor metric "Replicas" (per minute) showed the same story: 0 → 1 → 6 → 1 → 10.

Idle vs active billing

A replica is billed at the lower idle rate only when its revision runs at its minimum (≥ 1) and the replica processes no requests, uses less than 0.01 vCPU and receives less than 1,000 bytes per second. In West Europe, the idle vCPU rate is about an eighth of the active rate (USD 0.000004 vs 0.000034 per vCPU-second, Azure Retail Prices API, October 2026); memory costs the same in both states. Each subscription gets 180,000 vCPU-seconds, 360,000 GiB-seconds and 2 million requests free per month.

Left out of the video

Event-driven scaling with KEDA (queues, Event Hubs, cron, managed identity for scale rules), TCP scale rules, CPU and memory sizing (memory must be at least twice the CPU in GiB; memory over the limit restarts a replica, CPU over the limit throttles it), workload profiles, revision modes and traffic splitting. They are in the Microsoft Learn module below; queue scaling and traffic splitting get their own videos in this series.

Further reading

Video transcript

I sent one container app two kinds of traffic. Forty slow requests at a time. Then ten fast ones. Same scale rule, same limit of ten replicas, the running copies of the app. The replica counts were not what the setting's name made me expect. And when the traffic stopped, the replicas didn't leave.

In this video, you'll see what the HTTP scale rule counts, and why replicas stay after the traffic stops. And at the end, the setting that fixed a thirty-second first request.

Why did my app stay at one replica? The lab creates the app with a minimum of one replica and a maximum of one. To check, I asked Container Apps for the app's scale settings, the part that decides how many replicas run. Minimum one, maximum one, and no rules at all. I sent forty users at once, each request doing two seconds of work. One replica, forty users. Will anyone see an error? No errors. They just wait. This was my load test: forty clients, each request two seconds of work, for ninety seconds, while it counted the replicas. Not one request failed. But two seconds of work took over four seconds, and the replica count never moved. The app's web server handles sixteen requests at a time. The other twenty-four stand in line, and nothing tells you. Without a rule of your own, Container Apps uses a default HTTP rule, between zero and ten replicas. But the lab set both limits to one, so there was no room to scale. A scale rule tells Container Apps when to add or remove replicas. So I added the lab's HTTP rule, with one update command. A minimum of zero, and a maximum of ten. Then the rule: a name, and the type, HTTP. And a target of ten concurrent requests. A revision is a snapshot of your app's image and settings. Scale settings belong to it, so this change built a new revision, v2.

What does the scale rule actually count? The setting is called concurrent requests. So most people read it as: requests in flight right now, ten per replica. Let's test that. Test one: forty clients, each request takes two seconds. Test two: ten clients, each request takes fifty milliseconds. By that reading, test one needs four replicas, and test two needs one. So which test got more replicas? Test two. Ten replicas, the maximum, forty-two seconds in. Test one stopped at six. Here's why. The docs define the number differently. Every fifteen seconds, Container Apps counts the requests of the last fifteen seconds, and divides by fifteen. That's requests per second, not requests waiting. Picture a shop with a counter at the door. It clicks for every customer who comes in, not for the ones standing in line. Fast requests come back quickly, so the same ten clients sent a hundred and twenty requests per second. Divided by ten, that's twelve replicas, capped at the maximum of ten. The slow clients sent only about twenty per second, a sixth of the fast ones, so they got fewer replicas. It's called concurrent requests, but it's measured per second. The exact count is the platform's call. Twenty per second, divided by ten, would be two. So I ran the slow test again: forty clients, two seconds each, this time for nine minutes, to see if it settles. It didn't. It held six, from the first minute to the end. The docs call replica counts a target, not a guarantee. So the formula tells you the direction. Measure your own app under load. This series runs one Azure service at a time, for real. Subscribe to follow the series.

Why are replicas still running after the traffic stops? When the slow test ended, six replicas were running, and nothing came in anymore. When is the last replica gone? Nine minutes and forty-seven seconds after the last request. Scaling in waits for a five-minute window. Only when the lower count has held for five minutes do the extra replicas go. In my slow test, the watcher kept counting after the load stopped, until the app reached zero. Five and a half minutes after the last request, six became one, within fifteen seconds. And the last replica has a cool down of its own, five minutes by default, before the app goes to zero. At zero replicas, you pay no usage charges. But the next request has to wait for a cold start: a new replica, the image pulled, the app started. Then I sent three requests to the sleeping app, one after the other, and timed each one. The first took twenty-nine point six seconds. The next one took a tenth of a second. And while you wait for zero, every replica above the minimum is billed at the active rate, the full price.

What else surprised me in this run? First: a deployment during a peak. Right after the fast test, I changed only the minimum. The new revision started with ten replicas, as many as the old one had. Then I let it run without any traffic for six minutes, and kept counting. It kept all ten for five more minutes, then dropped to one. That's on purpose. A new revision has to carry the current load before it gets the traffic. So don't read those ten replicas as a scaling bug. Second: no ingress, the app's HTTP endpoint, and a minimum of zero. Without ingress, there are no requests to count, so the app scales to zero, and nothing can wake it up. Set a minimum of one, or scale on something else, like the length of a queue.

To sum it up. Container Apps only scales between your minimum and your maximum. The HTTP rule follows requests per second, not the requests in line. Extra replicas leave after five quiet minutes, and the last one after a cool down. And zero costs nothing, except the first user's wait.

And the setting from the start: a minimum of one replica. I set it, and let the app sit idle for six minutes. Then I sent the same three requests as before, one after the other. The first one took zero point one six seconds, instead of twenty-nine point six. A replica at its minimum that does nothing is billed at the idle rate. In West Europe, its CPU costs about an eighth of the active price, and memory the same as always. For an API that people wait on, that's often worth it. For work nobody waits on, keep zero.

New replicas only get traffic once they pass their readiness check. The video on screen now shows what that check really asks. 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