Home / Azure

AzureHow-toAdvanced

Azure Container Apps not scaling: fix KEDA scale rules for HTTP, Service Bus and queues

Replicas stay at zero or one while the queue grows. Read the scale config, fix rule metadata and scaler auth, and tune min/max replicas so Container Apps scales with load.

Deepak Bhardwaj8 min read

Applies toAzure Container Apps (Consumption and workload profiles environments), Azure CLI with the containerapp extension

The queue has 40,000 messages and the container app is still running one replica. Or it runs zero replicas and the first request after a quiet period times out. In Container Apps, scaling is handled by KEDA, driven by the scale block of the active revision. When that block is wrong, or KEDA can't authenticate to the event source, nothing scales and nothing obvious fails.

This guide shows how to read the scale config, set up working HTTP and Service Bus rules, and confirm KEDA is acting on them.

Symptom to cause

Symptom Likely cause Fix
Replicas never go above 1 maxReplicas is 1, or the rule is on an inactive revision Raise --max-replicas, check the active revision
Queue grows, replicas stay at 0 Scaler can't authenticate to Service Bus or Storage Fix --scale-rule-auth secret or --scale-rule-identity and the role
Rule exists but does nothing Wrong metadata key (queueName, namespace) or wrong queue name Correct the metadata; names are case-sensitive
HTTP app never scales out Ingress disabled, or concurrency threshold set too high Enable ingress, lower --scale-rule-http-concurrency
Cold start timeouts minReplicas is 0 on a latency-sensitive app Set --min-replicas 1 or more
Replicas flap up and down Threshold close to normal load, short cooldown Raise the threshold or the cooldown
CPU/memory rule doesn't scale to zero Expected: CPU and memory rules can't scale to zero Use an event or HTTP rule if you need zero

Step 1: read what is actually configured

Scale settings belong to the revision template. Check the active revision and its scale block:

az containerapp show -n ca-orders -g rg-apps \
  --query "{revisionMode:properties.configuration.activeRevisionsMode, latest:properties.latestRevisionName, ingress:properties.configuration.ingress.external, scale:properties.template.scale}" -o json

az containerapp revision list -n ca-orders -g rg-apps \
  --query "[].{name:name, active:properties.active, traffic:properties.trafficWeight, replicas:properties.replicas, health:properties.healthState}" -o table

In multiple revision mode, an older revision can still be taking traffic with an older scale config. Fix the scale rule, then move traffic to the new revision or deactivate the old one.

See the replicas running right now:

az containerapp replica list -n ca-orders -g rg-apps -o table

Step 2: set min and max replicas deliberately

az containerapp update -n ca-orders -g rg-apps \
  --min-replicas 1 --max-replicas 20
  • minReplicas 0 saves money but adds a cold start to the first request or message after idle. Use it for background workers, not for user-facing APIs.
  • maxReplicas also caps how hard you can hit a downstream database. Set it to what the database and its connection limit can take, not to the highest value allowed. Check the current per-app and per-environment limits in the docs; they differ by plan.

Warning

Changing anything under template, including scale settings, creates a new revision. In single revision mode, traffic moves to the new revision once it is healthy. Make the change in a quiet period or test it on a non-production app first.

Step 3: HTTP scaling

HTTP rules scale on concurrent requests per replica. Ingress must be enabled, because the platform counts requests at the ingress layer.

az containerapp update -n ca-api -g rg-apps \
  --min-replicas 2 --max-replicas 30 \
  --scale-rule-name http-concurrency \
  --scale-rule-type http \
  --scale-rule-http-concurrency 50

Pick the concurrency from a load test: the number of in-flight requests one replica handles before latency climbs. The default is low (check the current default for your API version), which is fine for slow handlers and too aggressive for fast ones.

Step 4: Service Bus scaling with managed identity

Event-driven rules use KEDA scalers. For Service Bus, KEDA reads the queue's message count and adds replicas when it goes over messageCount per replica.

Use a managed identity rather than a connection string. Assign a user-assigned identity to the app, give it a role on the namespace that can read queue metrics, and pass it to the rule:

IDENTITY_ID=$(az identity show -n id-ca-orders -g rg-apps --query id -o tsv)
PRINCIPAL_ID=$(az identity show -n id-ca-orders -g rg-apps --query principalId -o tsv)
SB_ID=$(az servicebus namespace show -n sb-prod-orders -g rg-msg --query id -o tsv)

az containerapp identity assign -n ca-orders -g rg-apps --user-assigned $IDENTITY_ID

az role assignment create --assignee-object-id $PRINCIPAL_ID \
  --assignee-principal-type ServicePrincipal \
  --role "Azure Service Bus Data Owner" --scope $SB_ID

az containerapp update -n ca-orders -g rg-apps \
  --min-replicas 0 --max-replicas 20 \
  --scale-rule-name sb-orders \
  --scale-rule-type azure-servicebus \
  --scale-rule-metadata "queueName=orders" "namespace=sb-prod-orders" "messageCount=20" \
  --scale-rule-identity $IDENTITY_ID

Microsoft's examples use Azure Service Bus Data Owner because the scaler needs to read entity runtime properties. Data Receiver alone is often not enough for the scaler, even though it's enough for the app to receive messages. If your security team pushes back, test a narrower role in non-production first.

--scale-rule-identity also accepts system for the system-assigned identity. Managed identity for scale rules is a newer feature: if your CLI doesn't recognise the flag, run az extension update --name containerapp.

Connection-string fallback

If you must use a secret, store it on the app and map it to the scaler's connection parameter:

az containerapp secret set -n ca-orders -g rg-apps \
  --secrets "sb-conn=<namespace-connection-string>"

az containerapp update -n ca-orders -g rg-apps \
  --scale-rule-name sb-orders \
  --scale-rule-type azure-servicebus \
  --scale-rule-metadata "queueName=orders" "messageCount=20" \
  --scale-rule-auth "connection=sb-conn"

Caution

When the connection string's key is rotated, the scaler stops working silently: replicas stay at the minimum while the queue backs up. If you rotate keys, update the secret in the same change, or move to managed identity.

Other scalers

The same pattern works for Storage queues (--scale-rule-type azure-queue, metadata queueName, queueLength, accountName) and Event Hubs (azure-eventhub). Metadata keys follow the KEDA scaler documentation for that scaler type. A misspelled key is ignored, not rejected, so copy the key names exactly.

Step 5: confirm KEDA is acting on the rule

System logs show scaler errors and scale decisions. From the CLI:

az containerapp logs show -n ca-orders -g rg-apps --type system --tail 100

In Log Analytics:

ContainerAppSystemLogs_CL
| where ContainerAppName_s == "ca-orders"
| where TimeGenerated > ago(1h)
| where Log_s has_any ("scale", "KEDA", "scaler", "unauthorized", "failed")
| project TimeGenerated, RevisionName_s, Reason_s, Log_s
| order by TimeGenerated desc

If the environment sends logs through diagnostic settings, the table is ContainerAppSystemLogs and the columns have no _s suffix (ContainerAppName, Log, Reason). Check which one your workspace has.

Watch the replica count against the queue depth to confirm the rule works:

APP_ID=$(az containerapp show -n ca-orders -g rg-apps --query id -o tsv)
az monitor metrics list --resource $APP_ID --metric Replicas \
  --interval PT1M --aggregation Maximum --offset 1h -o table

az servicebus queue show -g rg-msg --namespace-name sb-prod-orders -n orders \
  --query "countDetails.activeMessageCount"

Tuning notes

  • Threshold maths. With messageCount=20 and 400 active messages, KEDA targets about 20 replicas, capped by maxReplicas. If one replica clears 200 messages a minute, a threshold of 20 is much too low and you'll overshoot.
  • Polling and cooldown. KEDA checks scalers on a polling interval and waits a cooldown period before scaling back to zero. Newer API versions expose pollingInterval and cooldownPeriod on the scale block; check the defaults and whether your API version supports them before relying on them.
  • Graceful shutdown. Scale-in sends SIGTERM to replicas. Workers must stop pulling messages and finish or abandon in-flight ones within the termination grace period, or you'll see lock-lost retries and duplicates.
  • Multiple rules. If you set several rules, the app scales to the highest replica count any rule asks for.

Prevent it next time

  • Keep scale rules in Bicep or YAML with the app, not as portal edits, so a redeploy doesn't wipe them.
  • Use managed identity for scaler auth; it removes the key-rotation failure mode.
  • Alert when queue depth stays above a threshold for 10 minutes while Replicas is at its minimum. That combination means the scaler is broken.
  • Load test once to set HTTP concurrency and messageCount from measured per-replica throughput.
  • Review maxReplicas against downstream limits whenever the database tier or connection pool changes.