I put 200 messages into an Azure Storage queue for a worker on Azure Container Apps. For 22 minutes, not one of them was processed. Two fixes later, ten replicas cleared the same queue in about two minutes.
This explainer shows how a Container App finds out that messages are waiting (it doesn't: KEDA counts them for it), the two things my worker was missing, and where the real error was hiding. Everything below comes from a real run in my own Azure subscription.
Key points
- Without a scale rule, nothing counts your queue. A worker without ingress gets no HTTP requests, so the default HTTP rule has nothing to count. My worker kept the one replica it started with for fifteen minutes; the platform's log said "no triggers defined".
- A queue rule hands the counting to KEDA. Every 30 seconds KEDA asks the queue how many messages are waiting and divides by the queue length setting: a target per replica, not a threshold. 200 messages ÷ 10 = 20 replicas, capped at the maximum of 10.
- The rule's identity needs a role. The scale rule signs in with the app's managed identity, but an identity is only a badge. Without a data role on the storage account the scaler can't count, and the app shows no error. With Storage Queue Data Contributor it scaled from 1 to 10.
- Any message wakes the app; an empty queue lets it go. Three messages at zero replicas woke one replica in 41 seconds. After the queue was empty, all ten replicas stayed five more minutes, then went within 40 seconds.
Nothing is counting
A queue worker has no ingress: no public HTTP endpoint, nobody calls it, it pulls its work from the queue. I created mine like that first: no ingress, a minimum of zero, a maximum of ten, and no scale rule.
Container Apps scales on rules. The default rule counts HTTP requests, and a worker without ingress gets none. So nothing watched the queue: the app kept the one replica it started with, for fifteen minutes, while 200 messages waited. It went neither up to ten nor down to zero. The docs warn that an app like this can scale to zero and never start again; mine simply never changed. Either way, the queue doesn't move the count.
That one replica didn't process a single message either, which leads to the second problem below.
The queue rule
A scale rule is how you tell Container Apps what to watch. Underneath, it runs KEDA, an open-source autoscaler. For a queue rule KEDA polls every 30 seconds, asks the queue how many messages are waiting and computes desiredReplicas = ceil(messages ÷ queueLength), between your minimum and maximum. Picture a post room at night: the desks are the replicas, the mail tray is the queue, and KEDA is the inspector who counts the letters.
The rule I added, with the app's own system-assigned managed identity (no connection string):
az containerapp update -n queue-worker -g rg-demo \--scale-rule-name queue-scaling \--scale-rule-type azure-queue \--scale-rule-metadata accountName=mystorage \queueName=orders queueLength=10 \--scale-rule-identity system
The same in the template, as Container Apps stored it:
scale:minReplicas: 0maxReplicas: 10rules:- name: queue-scalingazureQueue:accountName: mystoragequeueName: ordersqueueLength: 10identity: system
The command from the docs failed on my machine. Azure CLI 2.88.0 answered unrecognized arguments: --scale-rule-identity system: that flag comes from the containerapp CLI extension (az extension add -n containerapp), which is in preview. With the extension (1.3.0b5) the rule went in.
The badge and the key
With the rule in place and the identity on, the next four minutes looked exactly like the first fifteen: one replica, 200 messages, no error on the app.
The scale rule authenticates with the app's identity. The identity is the badge; a role is the key. Without a role on the storage account, the door stays locked: KEDA can't read the queue length, and it scales nothing. The worker had the same problem. Its own log said, every ten seconds:
error: HttpResponseError: This request is not authorized to perform this operation using this permission.
The fix was one role assignment for the app's identity on the storage account:
principal=$(az containerapp show -n queue-worker -g rg-demo --query identity.principalId -o tsv)storageId=$(az storage account show -n mystorage -g rg-demo --query id -o tsv)az role assignment create --assignee-object-id $principal \--assignee-principal-type ServicePrincipal \--role "Storage Queue Data Contributor" --scope $storageId
Storage Queue Data Contributor lets the worker read and delete messages, and it let the scaler count them. 93 seconds after the role assignment, the first message was gone; 13 seconds later, ten replicas were running.
Ten replicas, then none
| One replica | Ten replicas (my run) | |
|---|---|---|
| 200 messages × 5 s of work | about 17 minutes | about 2 minutes after the first message |
| Messages per replica | 200 | 19 to 26 |
The queue was empty 223 seconds after the role assignment. All ten replicas stayed for five more minutes: KEDA waits five minutes after the queue is empty before it scales down to your minimum, here zero. Then they went within 40 seconds: 10 → 9 → 6 → 3 → 0.
The last test: at zero replicas, three messages, with the queue length still at ten per replica. One replica started 41 seconds later and the three messages were done 25 seconds after that. The queue length sets how many messages each replica should take, not when the first one starts. KEDA's default activation threshold for the queue is zero: any message wakes the app.
Where the real error was hiding
The app itself showed nothing: running, no error. The live system log stream (az containerapp logs show --type system) only said:
unable to get external metric azure-queue-orders for queue-worker: … encountered error
No word about permissions. The workspace behind the Container Apps environment had the full line, in the table ContainerAppSystemLogs_CL:
KEDAScalerFailed: GET https://mystorage.queue.core.windows.net/ordersRESPONSE 403: 403 This request is not authorized to perform this operation using this permission.ERROR CODE: AuthorizationPermissionMismatch
When a queue worker doesn't scale, query the system logs in Log Analytics, and check the role before anything else.
What happened when I ran it
Tested on October 20, 2026 in my own Azure subscription (westeurope, Azure CLI 2.88.0, containerapp extension 1.3.0b5); everything was deleted afterwards. The worker is my own Python app (azure-storage-queue with ManagedIdentityCredential): it takes one message, works on it for 5 seconds, deletes it. qwatch.py, also my own, sent the messages and polled the replica count and the queue length. The environment took 17.6 minutes to create, the cloud build 68 seconds, the app 41 seconds.
The first try, no ingress and no rule:
> az containerapp show -n queue-worker -g rg-demo --query "{ingress:properties.configuration.ingress, scale:properties.template.scale}" -o json{"ingress": null,"scale": {"cooldownPeriod": 300,"maxReplicas": 10,"minReplicas": 0,"pollingInterval": 30,"rules": null}}
Ten more minutes of it (after the first three):
3 s replicas 1 queue 200302 s replicas 1 queue 200602 s replicas 1 queue 200summary: first replica: 3 s | peak: 1 replicas at 3 s | queue empty: no | 0 replicas: not reached
After the role, the scale-up, the drain and the scale-in:
80 s replicas 1 queue 20093 s replicas 1 queue 199106 s replicas 10 queue 196223 s replicas 10 queue 0482 s replicas 10 queue 0494 s replicas 9 queue 0507 s replicas 6 queue 0520 s replicas 3 queue 0533 s replicas 0 queue 0
Three messages at zero:
28 s replicas 0 queue 341 s replicas 1 queue 366 s replicas 1 queue 0
My watcher polled about every 13 seconds, so the step from 1 to 10 replicas happened somewhere between 93 and 106 seconds; the docs describe scale-up steps of 1, 4, 8, 16 per evaluation, which my polls couldn't resolve. And one more detail: a CPU rule with a minimum of zero was accepted by the API (az containerapp update printed the rule with empty metadata in its response, the same display quirk as the HTTP rule in the previous video); the docs say CPU and memory rules can't scale to zero, which this run didn't test further.
Other scalers in the module
| Event source | Scale rule type | Metric per replica | Note |
|---|---|---|---|
| Azure Storage queue | azure-queue | queueLength | managed identity or connection string |
| Azure Service Bus | azure-servicebus | messageCount | 50 messages ÷ 5 = 10 replicas; topics need subscriptionName |
| Azure Event Hubs | azure-eventhub | unprocessedEventThreshold | more replicas than partitions don't help |
| Apache Kafka | kafka | lagThreshold | the partition count caps useful replicas |
| Cron | cron | desiredReplicas in a time window | with other rules, the highest count wins |





