• English icon
  • German icon
Erklärvideo

Wie entscheidet Azure Container Apps, wie viele Replikate laufen?

· Reihe: Container auf Azure

azureazure-container-appsautoscalingcontainersdevops

Das Video ist auf Englisch.

Ich habe dieselbe Container-App zweimal mit derselben HTTP-Skalierungsregel unter Last gesetzt. Vierzig langsame Requests gleichzeitig bekamen sechs Replikate. Zehn schnelle Requests gleichzeitig bekamen zehn, das Maximum. Mehr wartende Requests, weniger Replikate. Und als der Traffic aufhörte, blieb das letzte Replikat fast zehn Minuten.

Dieser Explainer zeigt, was die HTTP-Skalierungsregel in Azure Container Apps wirklich zählt, warum Replikate nach dem Traffic bleiben und was Scale to Zero den ersten Nutzer kostet. Alles hier stammt aus einem echten Lauf in meinem eigenen Azure-Abonnement. Das Video ist auf Englisch.

Das Wichtigste

  • Container Apps skaliert nur zwischen Minimum und Maximum. Die Übung aus Microsoft Learn startet die App mit min 1 und max 1: Vierzig Nutzer warteten, nichts lief auf einen Fehler, und die Zahl der Replikate bewegte sich nicht.
  • Die HTTP-Regel zählt Requests pro Sekunde, nicht wartende Requests. Alle 15 Sekunden nimmt Container Apps die Requests der letzten 15 Sekunden und teilt durch 15. Schnelle Requests erzeugen mehr Requests pro Sekunde und bekommen deshalb mehr Replikate.
  • Replikate gehen langsam. Zusätzliche Replikate gehen nach einem 5-Minuten-Fenster; das letzte hat eine eigene Abkühlzeit (Standard 5 Minuten). In meinem Lauf kam null 9 Minuten 47 Sekunden nach dem letzten Request.
  • Null kostet nichts, außer der Wartezeit des ersten Nutzers. Der erste Request nach null dauerte 29,6 Sekunden. Mit einem Minimum von eins waren es 0,16 Sekunden, und ein Replikat im Leerlauf wird günstiger abgerechnet.

Ein Replikat

Ein Replikat ist eine laufende Kopie Ihres Containers, und Hochskalieren heißt: mehr Kopien. Ohne eigene Skalierungsregel nutzt Container Apps eine Standard-HTTP-Regel zwischen null und zehn Replikaten. Die Übung setzt beide Grenzen auf eins, es gibt also keinen Spielraum.

Mit vierzig Nutzern gleichzeitig und zwei Sekunden Arbeit pro Request gab es keinen einzigen Fehler. Der Webserver der App (gunicorn mit 2 Workern × 8 Threads) bearbeitet sechzehn Requests gleichzeitig; die übrigen vierundzwanzig warteten. Zwei Sekunden Arbeit dauerten im Median 4,15 Sekunden, und die Zahl der Replikate blieb die vollen 90 Sekunden bei eins.

Die HTTP-Regel aus der Übung:

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

Dasselbe im Template:

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

Skalierungseinstellungen gehören zum Template der Revision, deshalb erzeugt diese Änderung eine neue Revision (hier agent-api--v2). Eine Revision ist ein Schnappschuss von Image und Einstellungen Ihrer App.

Was die Regel zählt

Die Einstellung heißt concurrentRequests und klingt nach „Requests in Bearbeitung, zehn pro Replikat“. So gelesen bräuchten vierzig langsame Clients vier Replikate und zehn schnelle eines. Der Lauf zeigt etwas anderes:

TestClientsArbeit pro RequestRequests pro SekundeReplikate
Langsam402 setwa 206
Schnell1050 msetwa 12010 (das Maximum)

Die Dokumentation definiert den Wert als Rate: alle 15 Sekunden die Requests der letzten 15 Sekunden geteilt durch 15. Gezählt werden die Kunden, die durch die Tür kommen, nicht die Schlange an der Kasse. Die schnellen Clients kamen schnell zurück und schickten etwa 120 Requests pro Sekunde: geteilt durch das Ziel von 10 sind das 12 Replikate, begrenzt auf das Maximum von 10.

Die langsamen Clients schickten nur etwa 20 pro Sekunde, ein Sechstel der schnellen, und bekamen deshalb weniger Replikate. Nach der Formel allein ergeben 20 geteilt durch 10 zwei. Ich habe den langsamen Test neun Minuten lang wiederholt, und er blieb bei sechs. Die Dokumentation nennt die Zahl der Replikate ein Ziel, keine Garantie. Die Formel zeigt die Richtung; testen Sie mit Ihren echten Antwortzeiten und beobachten Sie die Zahl der Replikate.

Wie das Hochskalieren laut Dokumentation abläuft: desiredReplicas = ceil(aktueller Wert ÷ Ziel), Schritte von 1, 4, 8, 16 … bis zum Maximum, ohne Wartezeit vor dem Hochskalieren. In meinem langsamen Test ging es innerhalb von 11 Sekunden von 1 auf 4, dann von 4 auf 6.

Wenn der Traffic aufhört

PhaseStandard laut DokumentationMein Lauf (langsamer Test)
Zusätzliche Replikate gehennach einem 300-Sekunden-Fenster6 → 1 nach 329–345 s ab dem letzten Request
Das letzte Replikat → nullnach der Abkühlzeit von 300 Sekundennach 587 s (9 min 47 s)
Erster Request nach nullein Kaltstart: neues Replikat, Image laden, App starten29,6 s, danach 0,14 s

Solange Sie auf null warten, wird jedes Replikat oberhalb des Minimums zum aktiven Tarif abgerechnet. Bei null Replikaten fallen keine Nutzungskosten an.

Zwei Überraschungen

Ein Deployment mitten in einer Lastspitze. Direkt nach dem schnellen Test habe ich nur das Minimum geändert (min 1, Revision v3). Die neue Revision startete mit zehn Replikaten, so vielen wie die alte, und hielt sie fünf weitere Minuten. Das ist der Wechsel ohne Ausfallzeit: Eine neue Revision muss die aktuelle Last tragen können, bevor sie den Traffic bekommt. Lesen Sie diese Replikate nicht als Skalierungsfehler.

Kein Ingress und ein Minimum von null. Ohne Ingress gibt es keine HTTP-Requests zum Zählen. Wenn Sie zusätzlich kein Minimum und keine andere Skalierungsregel definieren, skaliert die App auf null und kann nicht mehr starten. Setzen Sie ein Minimum von eins oder skalieren Sie auf etwas anderes, zum Beispiel die Länge einer Queue.

Was passierte, als ich es ausgeführt habe

Getestet am 20. Oktober 2026 in meinem eigenen Azure-Abonnement (westeurope, Azure CLI 2.88.0); danach wurde alles gelöscht. Die App ist die Mock-Agent-API aus der Microsoft-Learn-Übung (Flask, POST /agent/process wartet eine angegebene Zahl von Millisekunden); watch.py, mein eigenes Skript, erzeugte die Last über Keep-Alive-Clients und fragte alle paar Sekunden az containerapp replica list ab. Mein erster Versuch in germanywestcentral scheiterte an der Umgebung, mit demselben Fehler AKSCapacityHeavyUsage wie am Vortag. In westeurope dauerte die Umgebung 15,7 Minuten, der Cloud-Build 68 Sekunden, die App 41 Sekunden.

Der Start der Übung, min 1 und 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
}

Der schnelle Test (10 Clients × 50 ms); die ersten Requests warteten auf einen Kaltstart:

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

Der langsame Test, neun Minuten lang:

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 und der Kaltstart:

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

Mit einem Minimum von eins, nach sechs Minuten Leerlauf:

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

Zwei Details: az containerapp update gab in seiner Antwort "concurrentRequests": "" aus, während revision show danach den gespeicherten Wert "10" zeigte. Prüfen Sie also die Revision, nicht die Ausgabe des Updates. Und die Azure-Monitor-Metrik „Replicas“ (pro Minute) zeigte denselben Verlauf: 0 → 1 → 6 → 1 → 10.

Leerlauf- und Aktivtarif

Ein Replikat wird nur dann zum günstigeren Leerlauftarif abgerechnet, wenn seine Revision auf ihrem Minimum (≥ 1) läuft und das Replikat keine Requests bearbeitet, weniger als 0,01 vCPU nutzt und weniger als 1.000 Bytes pro Sekunde empfängt. In West Europe kostet die vCPU im Leerlauf etwa ein Achtel des aktiven Preises (USD 0,000004 gegenüber 0,000034 pro vCPU-Sekunde, Azure Retail Prices API, Oktober 2026); Arbeitsspeicher kostet in beiden Zuständen gleich viel. Jedes Abonnement hat pro Monat 180.000 vCPU-Sekunden, 360.000 GiB-Sekunden und 2 Millionen Requests frei.

Nicht im Video

Ereignisgesteuerte Skalierung mit KEDA (Queues, Event Hubs, Cron, Managed Identity für Skalierungsregeln), TCP-Regeln, CPU- und Speichergrößen (Speicher in GiB mindestens doppelt so viel wie CPU; Speicher über dem Limit startet ein Replikat neu, CPU über dem Limit wird gedrosselt), Workload-Profile, Revisionsmodi und Traffic-Splitting. Sie stehen im Microsoft-Learn-Modul unten; Queue-Skalierung und Traffic-Splitting bekommen in dieser Reihe eigene Videos.

Weiterführende Links

Video-Transkript (Englisch)

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.

Brauchen Sie das in Ihrer eigenen Azure-Umgebung?

Ich entwickle, migriere und betreibe .NET-Plattformen auf Azure, mit Azure DevOps und Terraform. Wenn Ihr Team das sauber aufsetzen möchte, sehen Sie sich mein Angebot an.

Zu den Leistungen