Container Apps: Scaling, Jobs and Dapr
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. See the section bibliography for the reference material consulted while preparing these pages. |
Examples on this page target Azure CLI 2.90.0 with the containerapp extension 1.3.0b5 (still required for several
of the preview flags used below — run az extension add --name containerapp --upgrade first). Azure Container
Apps scales container apps and event-driven jobs internally using KEDA (Kubernetes Event-driven Autoscaling); the
service does not publish a single fixed KEDA version reader-visible in the CLI or portal the way an AKS cluster
does (AKS’s own managed KEDA add-on, by contrast, ships 2.19 on AKS 1.36 and 2.20 on AKS 1.37 — see
Deploying Microservices on AKS), so treat the scale-rule behaviour described below (polling interval,
cooldown, stabilization windows, scale-up steps) as the platform’s current documented contract rather than a
version-pinned one, and re-verify it against the References before relying on exact numbers in production. This
page assumes the Container Apps environment, the catalog-api and orders-api apps, and revisions are already in
place from Container Apps — that page covers the environment, app and revision basics and
is not repeated here.
The Bookshelf scaling and jobs scenario
Bookshelf adds one more moving part on top of cae-bookshelf, catalog-api and orders-api: a notifier
worker that sends order-confirmation emails and push notifications. Rather than run notifier as an always-on
container app polling a queue, Bookshelf runs it as an event-driven job — a com.example.bookshelf.notifier
Java process, packaged the same way as catalog-api and orders-api (see
Azure Container Registry for how its image reaches acrbookshelf), that Container Apps starts
on demand, once per queued message, and tears down again once the message is processed. catalog-api and
orders-api keep scaling on HTTP concurrency as covered in Container Apps; this page
adds a Service Bus-based custom scale rule for notifier and, separately, a Dapr pub/sub component so
orders-api can publish an order-placed event without knowing notifier exists.
Shell variables used throughout (matching the rest of this section):
export RG=rg-bookshelf
export LOCATION=eastus2
export ENV=cae-bookshelf
export APP=catalog-api
Scaling: KEDA-based rules
Scale definition: limits, rules and behavior
Every container app’s scaling is the combination of three things: limits (--min-replicas /
--max-replicas, 0-1000, defaulting to 0 minimum and 10 maximum), rules (HTTP, TCP or custom, evaluated with
OR semantics — the app scales once any rule’s condition is met), and behavior (the fixed platform values
below that turn a rule’s measurement into an actual replica count change):
| Behavior | Documented default |
|---|---|
Polling interval |
30 seconds — how often KEDA queries an event source; does not apply to HTTP/TCP rules, which are measured continuously from ingress traffic instead. |
Cooldown period |
300 seconds — how long after the last event KEDA waits before scaling down to the minimum replica count. It only governs the final step down to the configured minimum, not every intermediate scale-in step. |
Scale-up stabilization window |
0 seconds — KEDA acts on a scale-up decision immediately. |
Scale-down stabilization window |
300 seconds — a scale-down condition must hold for this long before KEDA acts on it. |
Scale-up step |
1, then 4, 8, 16, 32, … up to the configured maximum — replicas are not added one at a time once demand is sustained; the step itself doubles each cycle. |
Scale-down step |
100% of the replicas KEDA has decided are no longer needed, removed in one step. |
Scaling algorithm |
|
HTTP concurrency
An HTTP scale rule adds a replica once the number of concurrent requests (measured as requests over the last 15
seconds, divided by 15) exceeds a threshold, up to --max-replicas. catalog-api and orders-api already use
this rule as their default — see Container Apps — and it is the platform’s implicit
default (threshold 10, min 0, max 10) if no scale rule is defined at all.
In the Azure portal
-
Open the container app → Scale → Edit and deploy.
-
On the Scale tab, set the minimum and maximum replica range.
-
Select Add, name the rule, choose HTTP Scaling from the Type dropdown, and enter the concurrent request threshold.
With the Azure CLI
az containerapp create \
--name $APP --resource-group $RG --environment $ENV \
--image acrbookshelf.azurecr.io/catalog-api:1.0.0 \
--min-replicas 0 --max-replicas 5 \
--scale-rule-name http-rule \
--scale-rule-type http \
--scale-rule-http-concurrency 100
References: Scaling in Azure Container Apps — HTTP.
TCP concurrency
A TCP rule works the same way for raw TCP ingress traffic (concurrent connections over the last 15 seconds,
divided by 15). It requires --ingress and --transport tcp to be set, and has no Azure portal equivalent.
With the Azure CLI
az containerapp create \
--name orders-tcp-listener --resource-group $RG --environment $ENV \
--image acrbookshelf.azurecr.io/orders-api:1.0.0 \
--min-replicas 0 --max-replicas 5 \
--transport tcp --ingress external --target-port 5005 \
--scale-rule-name tcp-rule \
--scale-rule-type tcp \
--scale-rule-tcp-concurrency 100
References: Scaling in Azure Container Apps — TCP.
Custom scalers (Service Bus, Event Hubs, Storage Queue, Cron)
A custom scale rule wraps any KEDA ScaledObject scaler — Azure Service Bus, Azure Event Hubs, Azure Queue
Storage, Kafka, Redis, a cron schedule, and many more. --scale-rule-type takes the KEDA scaler’s own type
value (for example azure-servicebus, azure-eventhub, azure-queue, or cron), and --scale-rule-metadata
takes that scaler’s metadata block as space-separated key=value pairs.
Bookshelf’s notifier job (built out fully under Jobs: manual, scheduled and event-driven below) scales on the depth of a Service Bus queue
named orders-notify. As a container-app-style custom rule (the same rule shape also applies to an always-on
worker, if Bookshelf ever needed one instead of a job):
In the Azure portal
-
Open the app or job → Scale → Edit and deploy → Scale tab → Add.
-
From the Type dropdown, select Custom, and enter the KEDA scaler’s
typevalue (e.g.azure-servicebus) in the Custom rule type box. -
Under Metadata, add one entry per key in the scaler’s
metadatablock. -
Managed-identity authentication for a custom scale rule is not configurable from the portal — use the CLI or an ARM/Bicep template instead.
With the Azure CLI
az containerapp create \
--name notifier-worker --resource-group $RG --environment $ENV \
--image acrbookshelf.azurecr.io/notifier:1.0.0 \
--user-assigned "$(az identity show --name id-notifier --resource-group $RG --query id -o tsv)" \
--min-replicas 0 --max-replicas 10 \
--scale-rule-name servicebus-orders-notify \
--scale-rule-type azure-servicebus \
--scale-rule-metadata "queueName=orders-notify" "namespace=sb-bookshelf" "messageCount=5" \
--scale-rule-identity "$(az identity show --name id-notifier --resource-group $RG --query id -o tsv)"
A cron custom scale rule (time-window scaling, distinct from a scheduled job’s cron trigger under Jobs: manual, scheduled and event-driven)
takes start, end, timezone and desiredReplicas metadata instead of a queue depth:
az containerapp update \
--name catalog-api --resource-group $RG \
--scale-rule-name business-hours \
--scale-rule-type cron \
--scale-rule-metadata "timezone=Europe/Madrid" "start=0 8 * * 1-5" "end=0 20 * * 1-5" "desiredReplicas=3"
Managed-identity scaler authentication
Custom scale rules for Azure resources (Service Bus, Storage Queue, Event Hubs) accept either a Container Apps
secret referenced through --scale-rule-auth (a connection string) or a managed identity referenced through
--scale-rule-identity. Prefer the managed-identity form: it removes a Service Bus connection string from the
app’s configuration entirely, matching the managed-identity-first approach in
Identity and Access. Grant the identity the data-plane role the scaler needs (for
Service Bus, Azure Service Bus Data Receiver scoped to the namespace or queue) before attaching it:
az role assignment create \
--assignee-object-id "$(az identity show --name id-notifier --resource-group $RG --query principalId -o tsv)" \
--assignee-principal-type ServicePrincipal \
--role "Azure Service Bus Data Receiver" \
--scope "$(az servicebus namespace show --name sb-bookshelf --resource-group $RG --query id -o tsv)"
--scale-rule-identity takes either a user-assigned identity’s full resource ID (as used for notifier above)
or the literal value system to use the app’s system-assigned identity instead. Secrets/environment variables
consumed by the running container itself — as opposed to the scaler’s own authentication — are covered on
Key Vault and Secrets and Runtime Configuration; this page only
covers how the scaler authenticates to read the queue’s depth.
The KEDA scale loop
The one pitfall worth calling out on its own: a container app with no ingress and a minimum of zero
replicas, and no scale rule defined, has no trigger of any kind to wake it back up — HTTP/TCP scaling needs
ingress traffic to measure, and with no custom rule there is nothing else polling an event source either. The
platform’s own guidance is blunt about this: define a scale rule, or set --min-replicas 1 or higher, on any
app that has ingress disabled. notifier avoids the trap because it is a job, not an app — its event-driven
trigger is precisely the Service Bus custom scale rule, which is what wakes it from zero in the first place, and
jobs do not carry ingress at all.
Jobs: manual, scheduled and event-driven
A Container Apps job is a task that starts, runs for a finite duration, and stops — unlike an app, a failed
job replica is not automatically restarted into a long-running process; it is retried up to a limit and then the
execution is marked failed. Jobs and apps share the same environment (cae-bookshelf), networking and logging,
but jobs do not support ingress, custom domains/SSL, or Dapr — three restrictions worth remembering before
reaching for a job where an always-on app (or a Dapr-enabled one) is really what is needed.
Three trigger types cover Bookshelf’s needs:
| Trigger type | Bookshelf use |
|---|---|
Manual |
An on-demand backfill or data-migration task, started from the CLI, portal, or an ARM/REST call. |
Schedule |
A nightly |
Event |
|
In the Azure portal
-
Search Container App Jobs → Create.
-
On the Basics tab, choose the trigger type: Manual, Schedule (enter a cron expression, evaluated in UTC), or Event-driven (configure a scale rule the same way as a custom app scale rule).
-
On the Container tab, set the image, command, CPU/memory, and environment variables.
-
Under Settings → Configuration (after creation), adjust replica timeout, retry limit, parallelism and replica completion count.
-
To run a job on demand regardless of its trigger type, open the job’s overview page and select Run now.
-
To inspect past runs, open Monitoring → Execution history (the platform retains the most recent 100 successful and 100 failed executions; older executions must be queried from the environment’s log destination).
With the Azure CLI
Manual job (an ad hoc catalog backfill):
az containerapp job create \
--name catalog-backfill --resource-group $RG --environment $ENV \
--trigger-type Manual \
--replica-timeout 1800 --replica-retry-limit 0 \
--replica-completion-count 1 --parallelism 1 \
--image acrbookshelf.azurecr.io/catalog-backfill:1.0.0 \
--cpu 0.5 --memory 1.0Gi
Scheduled job (catalog-report, nightly at 02:00 UTC — note that a cron expression on this page is always
UTC, not the reader’s local timezone):
az containerapp job create \
--name catalog-report --resource-group $RG --environment $ENV \
--trigger-type Schedule \
--cron-expression "0 2 * * *" \
--replica-timeout 900 --replica-retry-limit 1 \
--replica-completion-count 1 --parallelism 1 \
--image acrbookshelf.azurecr.io/catalog-report:1.0.0 \
--cpu 0.5 --memory 1.0Gi
Event-driven job (notifier, one execution per queued message, scaling to zero when idle — this is the job
this page centers its scaling story on):
az containerapp job create \
--name notifier --resource-group $RG --environment $ENV \
--trigger-type Event \
--replica-timeout 300 --replica-retry-limit 3 \
--parallelism 1 \
--image acrbookshelf.azurecr.io/notifier:1.0.0 \
--cpu 0.25 --memory 0.5Gi \
--user-assigned "$(az identity show --name id-notifier --resource-group $RG --query id -o tsv)" \
--min-executions 0 --max-executions 10 --polling-interval 30 \
--scale-rule-name servicebus-orders-notify \
--scale-rule-type azure-servicebus \
--scale-rule-metadata "queueName=orders-notify" "namespace=sb-bookshelf" "messageCount=1" \
--scale-rule-identity "$(az identity show --name id-notifier --resource-group $RG --query id -o tsv)"
Start any job execution on demand, inspect history, and check a single execution’s status:
az containerapp job start --name notifier --resource-group $RG
az containerapp job execution list --name notifier --resource-group $RG --output table
az containerapp job execution list \
--name notifier --resource-group $RG \
--query "[0].{status:properties.status, startTime:properties.startTime}" --output table
az containerapp job start also accepts --yaml <file> to override the container image, command, environment
variables or arguments for a single execution without changing the job’s stored configuration — useful for
running catalog-backfill once against a different target table without redefining the job. Because starting a
job can expose any secret or managed identity available to its containers through that override template,
restrict Microsoft.App/jobs/start/action (bundled into the built-in Container Apps Jobs Operator and
Container Apps Jobs Contributor roles) to identities Bookshelf actually trusts with those secrets, per
Identity and Access.
The settings that shape every job execution, regardless of trigger type:
| Setting | Meaning |
|---|---|
|
Maximum seconds a replica may run before it is forcibly stopped and treated as failed. |
|
How many times a failed replica is retried before the execution is marked failed; |
|
How many replicas run concurrently per execution; |
|
How many of those parallel replicas must succeed for the execution as a whole to succeed; must be
|
|
(Event-driven jobs only) how often, in seconds, KEDA checks the scale rule’s event source; defaults to 30. |
Dapr: sidecar, building blocks and components
Dapr (Distributed Application Runtime) runs as a sidecar container next to each enabled app’s own container,
giving it a local HTTP/gRPC API for cross-cutting concerns — service invocation, pub/sub messaging, state
management, secrets, bindings and actors — without linking a Dapr SDK into application code (though SDKs exist
and are the more ergonomic choice from Java/.NET). Container Apps manages the sidecar’s lifecycle automatically
once Dapr is enabled on an app; jobs cannot use Dapr at all, which is why notifier reaches Bookshelf’s
messaging exclusively through its Service Bus scale rule rather than through a Dapr pub/sub subscription.
In the Azure portal
-
Open
orders-api→ Dapr configuration. -
Toggle Enabled.
-
Set App ID (
orders-api), App port (the container’s listening port), and App protocol (httporgrpc). -
Select Save, which creates a new revision with the sidecar attached.
With the Azure CLI
az containerapp create \
--name orders-api --resource-group $RG --environment $ENV \
--image acrbookshelf.azurecr.io/orders-api:1.0.0 \
--target-port 8080 --ingress internal \
--enable-dapr \
--dapr-app-id orders-api \
--dapr-app-port 8080 \
--dapr-app-protocol http
Components: az containerapp env dapr-component set
A Dapr component is a YAML manifest — state store, pub/sub broker, secret store, or binding — registered
once on the environment and then scoped to the apps allowed to use it, so catalog-api and orders-api share
one pub/sub component definition without either one embedding Service Bus configuration directly. The manifest’s
shape mirrors upstream Dapr components almost exactly (componentType, version, metadata, an optional
secrets block, and a scopes list), which is why component YAML found in Dapr’s own documentation ports
directly to Container Apps with only the CLI invocation changing.
Bookshelf’s orders-placed pub/sub component, authenticating to Service Bus with `orders-api’s managed
identity rather than a connection string:
# pubsub-orders.yaml
componentType: pubsub.azure.servicebus.topics
version: v1
metadata:
- name: namespaceName
value: "sb-bookshelf.servicebus.windows.net"
- name: azureClientId
value: "<orders-api-user-assigned-identity-client-id>"
scopes:
- orders-api
- notifier-subscriber
With the Azure CLI
az containerapp env dapr-component set \
--name $ENV --resource-group $RG \
--dapr-component-name pubsub-orders \
--yaml pubsub-orders.yaml
az containerapp env dapr-component list --name $ENV --resource-group $RG --output table
az containerapp env dapr-component show \
--name $ENV --resource-group $RG --dapr-component-name pubsub-orders
Service invocation between Bookshelf services
With the sidecar enabled on both apps, catalog-api calls orders-api through Dapr’s own service invocation
building block instead of resolving `orders-api’s internal ingress URL directly — the call is addressed by
Dapr app ID and travels sidecar-to-sidecar, which gives it built-in mTLS, retries and distributed tracing for
free:
// From catalog-api, calling orders-api's /orders endpoint via the local Dapr sidecar
WebClient daprClient = WebClient.create("http://localhost:3500");
Order order = daprClient.get()
.uri("/v1.0/invoke/orders-api/method/orders/{id}", orderId)
.retrieve()
.bodyToMono(Order.class)
.block();
Pub/sub over Service Bus
orders-api publishes an order-placed event through the pubsub-orders component above once an order is
persisted; any Dapr-enabled subscriber scoped to the same component (a future notification app, or a Dapr-side
consumer standing in for notifier in an environment that does not need a job) declares a subscription and
receives the event pushed to it, rather than polling. notifier itself, as an event-driven job, does not
participate in this Dapr subscription — it reaches the same Service Bus topic/queue directly through its scale
rule and the Service Bus SDK, precisely because jobs cannot run the Dapr sidecar. General Service Bus/AMQP
messaging patterns beyond this Dapr-specific angle are covered in Messaging Protocols.
Dapr add-on concepts specific to AKS (the Dapr extension for AKS, alongside the KEDA add-on) are covered in Deploying Microservices on AKS rather than repeated here; this page only covers Dapr as Container Apps exposes it directly.
Health probes
Container Apps supports the same three probe types Kubernetes does, each governing a different failure response:
| Probe | Effect on failure |
|---|---|
Liveness |
The platform restarts the replica’s container. |
Readiness |
The replica is removed from the ingress load balancer (not restarted) until it reports ready again. |
Startup |
While a startup probe is active, liveness and readiness probes are disabled — it exists to give a slow-booting container (a JVM warming up, a Flyway migration running) time to finish without being killed by an impatient liveness probe or marked unready by a readiness probe with too short an initial delay. |
There is no dedicated CLI flag surface for probes (no --liveness-probe-* family of parameters); they are set
through the container app’s YAML template, either applied at creation or via an update.
In the Azure portal
-
Open the app → Containers → select the container → Health probes tab.
-
Add a probe, choose its type (Liveness, Readiness, Startup), the check mechanism (HTTP, TCP socket, or exec), the path/port, and timing (initial delay, period, failure threshold).
With the Azure CLI
# probes.yaml (partial container template)
containers:
- name: orders-api
image: acrbookshelf.azurecr.io/orders-api:1.0.0
probes:
- type: startup
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 12
- type: liveness
httpGet:
path: /actuator/health/liveness
port: 8080
periodSeconds: 15
failureThreshold: 3
- type: readiness
httpGet:
path: /actuator/health/readiness
port: 8080
periodSeconds: 10
failureThreshold: 3
az containerapp update --name orders-api --resource-group $RG --yaml probes.yaml
References: Health probes in Azure Container Apps.
Volumes
Container Apps mounts three kinds of volume into a replica’s containers, none of which is meant to replace a managed database — they cover local scratch space, a shared network file share, and secret material:
| Volume type | Use and lifetime |
|---|---|
Ephemeral ( |
Local scratch space shared between containers of the same replica (main container plus any sidecar/init containers). Contents survive a container restart within the replica but are gone once the replica itself is removed — never durable, never shared across replicas. |
Azure Files ( |
A file share from an Azure Files storage account, registered once on the environment and then mounted by name
( |
Secret volume |
A volume with |
In the Azure portal
-
Open the app → Volumes (for Azure Files: register the storage first at the environment level under Storage → Add, naming it and pointing it at the storage account and file share).
-
On the app, select Add volume, choose the type (EmptyDir, Azure File Share, or Secret), and for Azure Files select the environment-level storage name registered above.
-
Under Containers → the container’s Volume mounts tab, mount the volume at a path inside the container.
With the Azure CLI
Register an Azure Files share on the environment once, then reference it by name from any app’s YAML template:
az containerapp env storage set \
--name $ENV --resource-group $RG \
--storage-name bookshelf-reports \
--azure-file-account-name stbookshelf \
--azure-file-account-key "$(az storage account keys list --account-name stbookshelf --resource-group $RG --query [0].value -o tsv)" \
--azure-file-share-name reports \
--access-mode ReadWrite
# volumes.yaml (partial container template)
volumes:
- name: scratch
storageType: EmptyDir
- name: reports
storageType: AzureFile
storageName: bookshelf-reports
- name: tls-cert
storageType: Secret
secrets:
- secretRef: orders-api-tls-cert
path: tls.crt
containers:
- name: catalog-report
image: acrbookshelf.azurecr.io/catalog-report:1.0.0
volumeMounts:
- volumeName: scratch
mountPath: /tmp/work
- volumeName: reports
mountPath: /mnt/reports
az containerapp job update --name catalog-report --resource-group $RG --yaml volumes.yaml
Networking: VNet, private endpoint, internal environment, UDR/NAT
By default, a Container Apps workload profiles environment (the current default environment type) still gets its own managed virtual network, but every feature in this section — bringing an existing VNet, a private endpoint, an internal-only environment, user-defined routes and NAT — requires the environment to sit in a custom VNet with a dedicated, delegated subnet. Consumption-only (legacy) environments support a custom VNet too, but not UDR, NAT gateway egress control, or private endpoints — those three are workload-profiles-only.
| Feature | What it changes |
|---|---|
Custom VNet |
The environment’s infrastructure and (optionally) apps run inside a subnet Bookshelf owns and controls,
instead of a Microsoft-managed one — required before any of the rows below apply. Minimum subnet size: |
Internal environment |
|
Private endpoint |
A private endpoint on the environment lets other Azure VNets (not necessarily peered) reach it through Azure
Private Link, without the environment itself needing to be |
User-defined routes (UDR) / NAT gateway |
A UDR on the delegated subnet forces the environment’s egress traffic through a network virtual appliance or firewall instead of straight to the internet; a NAT gateway on the same subnet instead gives all outbound traffic one static public IP, which is the simpler option when the goal is just an allow-list-able egress IP rather than full traffic inspection. Both are workload-profiles-only, and Container Apps does not support the older KEDA HTTP proxy settings as an alternative route for this — a NAT gateway or UDR-routed appliance is the supported path. |
In the Azure portal
-
When creating the environment (or a new one to migrate into), on the Networking tab select Custom VNet, choose the delegated subnet, and set the VNet integration to Internal or External.
-
To attach a private endpoint after creation, open the environment → Networking → Private endpoint connections → Add.
-
To route egress through a NAT gateway, create the NAT gateway resource, associate it with the environment’s delegated subnet from the Subnets blade of the VNet (not from the Container Apps environment blade itself).
With the Azure CLI
az containerapp env create \
--name $ENV --resource-group $RG --location $LOCATION \
--infrastructure-subnet-resource-id "$(az network vnet subnet show --vnet-name vnet-bookshelf --name snet-cae --resource-group $RG --query id -o tsv)" \
--internal-only true
az network private-endpoint create \
--name pe-cae-bookshelf --resource-group $RG \
--vnet-name vnet-bookshelf --subnet snet-private-endpoints \
--private-connection-resource-id "$(az containerapp env show --name $ENV --resource-group $RG --query id -o tsv)" \
--group-id managedEnvironments \
--connection-name cae-bookshelf-connection
References: Networking in an Azure Container Apps environment, Configure virtual networks in Azure Container Apps environments, Use a private endpoint with an Azure Container Apps environment.
Deeper VNet peering, AKS-side networking (CNI plugins, network policies) and general Azure networking concepts belong to AKS Clusters and are not repeated here; this section only covers the Container-Apps-specific surface — environment-level custom VNet, internal-only, private endpoint, and UDR/NAT egress control.
Pricing model
Container Apps bills the Consumption plan per vCPU-second and GiB-second actually used while a replica is
running, plus a per-request charge above a monthly free grant, and charges nothing while an app or job execution
is scaled to zero — which is the direct economic reason notifier is worth running as an event-driven job
rather than an always-on worker: it only consumes compute for the seconds it is actually processing a message.
A replica that is idle but still resident in memory (below the minimum replica count, or kept warm during a
scale-down stabilization window) can be billed at a lower "idle charge" rate rather than the full active rate,
which is worth remembering when comparing an app kept at --min-replicas 1 against one left free to scale to
zero. The Dedicated workload-profile plan instead bills for reserved vCPU/memory on dedicated instances
regardless of utilization, and is the plan Bookshelf would reach for only if catalog-api or orders-api needed
guaranteed capacity, larger instance sizes, or the isolation of not sharing underlying nodes with other
customers' workloads — Bookshelf’s Consumption-only workloads described throughout this section do not need it.
Jobs bill the same per-second Consumption model as apps, scoped to each execution’s actual running replicas
rather than to the job definition itself, so a scheduled job that never has work to do (a catalog-report run
that finds nothing new) still bills for the seconds its replica took to start up, run its check, and exit, but
nothing between runs. Dapr’s sidecar container adds its own small vCPU/memory footprint to every replica it runs
alongside, which is itself billed as part of that replica’s consumption.
Troubleshooting
A scale rule that never seems to fire is the most common failure mode, and it is almost always one of: the app
has ingress disabled, --min-replicas 0, and no custom scale rule at all (the pitfall called out under the
KEDA scale loop diagram above — nothing is polling anything, so the app never wakes); the custom scale rule’s
--scale-rule-identity principal lacks the data-plane role needed to read the event source (a Service Bus scaler
needs at least Azure Service Bus Data Receiver to read queue depth, not just Data Owner on the namespace for
unrelated reasons); or the scale rule’s metadata does not match the event source’s actual resource names
(a typo’d queueName silently measures depth 0 forever rather than erroring). az containerapp revision list
and the app’s own Log stream blade (or whichever log destination the environment forwards to) show KEDA’s
own scaler errors, which name the exact metadata key or authentication failure.
A job execution stuck in Running past its expected duration is usually --replica-timeout set too high for
the actual workload, masking a hung container that should have failed fast; a job that fails every execution with
zero retries attempted is --replica-retry-limit 0, which is correct for anything that must not silently repeat
side effects (an email send, for instance) but wrong for anything that should tolerate a transient dependency
outage. az containerapp job execution list plus az containerapp job logs show (or the log destination)
narrow this down per execution.
A Dapr-enabled app that cannot reach a component almost always traces to one of: the component’s scopes list
does not include the calling app’s Dapr app ID (a component with no scopes is available to every Dapr-enabled
app in the environment, which is a security smell more often than an intended default); the app was not restarted
after the component was added (a component change, like a scale-rule change, only takes effect on the next
revision); or, for managed-identity component authentication, the identity referenced in the component YAML
lacks the target service’s data-plane role, the same class of failure as a scale rule’s --scale-rule-identity.
An internal or custom-VNet environment that an app cannot reach from outside the VNet is very often working
exactly as configured — --internal-only true intentionally removes the public endpoint — so the fix is
usually a VPN/ExpressRoute/peering path into the VNet, or a private endpoint from the calling network, not a
setting to revert. A NAT gateway or UDR that leaves the environment unable to reach anything at all typically
means the route table or NAT association was placed on the wrong subnet, or omits the return path the
environment’s control-plane traffic needs; re-check against the exact subnet association the References below
call out before assuming the environment itself is broken.