Reduce Azure Container Apps compute costs by 40-80% through scale-to-zero configuration, workload profile optimization, cron-based scheduling, and internal ingress routing.
An MSP managing 12 Container Apps environments across customer tenants found that 8 of them were paying for compute that should have been free. The culprit: minimum replica counts defaulting to 1 instead of 0, keeping instances running and billing 24/7. After a systematic review, their combined monthly spend dropped from $2,400 to $600.
Azure Container Apps looks inexpensive on paper, but many teams quietly burn hundreds or thousands per month due to small configuration defaults. Azure Container Apps provides generous free monthly grants — 180,000 vCPU-seconds, 360,000 GiB-seconds, and 2 million HTTP requests — but most deployments exceed those allowances unnecessarily because of always-on replicas, over-provisioned resources, and misconfigured ingress.
This guide addresses the most common sources of unnecessary Container Apps spend:
Every Azure subscription receives a free monthly grant for Container Apps in Consumption plan environments. The key to staying within these limits is ensuring your containers scale to zero when not serving traffic.
1. Scale-to-zero configuration with Bicep:
// Bicep template — Container App with scale-to-zero
resource containerApp 'Microsoft.App/containerApps@2023-05-01' = {
name: 'my-api'
location: location
properties: {
environmentId: environment.id
configuration: {
ingress: {
external: false // internal-only saves request charges
targetPort: 8080
}
}
template: {
containers: [
{
name: 'api'
image: 'myregistry.azurecr.io/api:latest'
resources: {
cpu: json('0.25') // minimum allocation
memory: '0.5Gi'
}
}
]
scale: {
minReplicas: 0 // scale to zero when idle
maxReplicas: 10
rules: [
{
name: 'http-scaling'
http: {
metadata: {
concurrentRequests: '50'
}
}
}
]
}
}
}
}Key insight: Setting minReplicas: 0 is the single highest-impact change. A container that scales to zero during idle periods can stay entirely within the free grant for low-traffic workloads.
2. Consumption vs Dedicated plan evaluation:
Consumption plan charges per-second for active replicas. Dedicated plan charges for reserved node capacity regardless of usage. Choose Consumption for bursty or low-traffic workloads, and Dedicated only when you need GPU support, larger instance sizes, or predictable high-utilization workloads.
| Factor | Consumption Plan | Dedicated Plan |
|---|---|---|
| Billing model | Per-second for active replicas | Reserved node capacity |
| Scale-to-zero | Yes (no charge when idle) | Node still billed |
| Free grant | 180K vCPU-s, 360K GiB-s | None |
| Best for | Bursty, low-traffic, event-driven | Sustained high utilization, GPU |
| Cold start | Yes (from zero replicas) | Minimal (nodes pre-provisioned) |
Container Apps supports a range of CPU and memory combinations. The default allocation is often far more than lightweight APIs or background workers need.
1. Lightweight workloads optimization:
For simple REST APIs, webhook receivers, or queue processors, use the minimum allocation of 0.25 vCPU and 0.5 GiB memory. This is sufficient for most Node.js, Python, or Go microservices and cuts per-replica cost by 75% compared to the 1 vCPU default many teams deploy with.
# Azure CLI — update container resource allocation
az containerapp update \
--name my-api \
--resource-group rg-production \
--cpu 0.25 \
--memory 0.5Gi
# Verify current allocation
az containerapp show \
--name my-api \
--resource-group rg-production \
--query "properties.template.containers[0].resources"2. Autoscale rules configuration:
HTTP-based scaling is the default, but many workloads benefit from custom KEDA scalers tied to queue depth, CPU utilization, or custom metrics. The key is setting appropriate thresholds that prevent premature scale-out.
// Bicep — autoscale with multiple rules
scale: {
minReplicas: 0
maxReplicas: 20
rules: [
{
name: 'http-rule'
http: {
metadata: {
concurrentRequests: '100' // scale at 100 concurrent, not default 10
}
}
}
{
name: 'queue-rule'
custom: {
type: 'azure-servicebus'
metadata: {
queueName: 'orders'
namespace: 'sb-production'
messageCount: '50' // scale per 50 messages
}
auth: [
{
secretRef: 'sb-connection'
triggerParameter: 'connection'
}
]
}
}
]
}Warning: Setting HTTP concurrent requests too low (e.g., 10) causes aggressive scale-out. For most APIs handling typical web traffic, 50-100 concurrent requests per replica is a better starting point.
Many Container Apps workloads have predictable usage patterns. Configuring time-based scaling prevents paying for capacity during known idle periods.
1. Cron-based scheduling for off-hours:
// Bicep — KEDA cron scaler for business-hours scaling
scale: {
minReplicas: 0
maxReplicas: 10
rules: [
{
name: 'business-hours'
custom: {
type: 'cron'
metadata: {
timezone: 'America/New_York'
start: '0 8 * * 1-5' // Mon-Fri 8 AM
end: '0 18 * * 1-5' // Mon-Fri 6 PM
desiredReplicas: '2' // keep 2 warm during business hours
}
}
}
{
name: 'http-rule'
http: {
metadata: {
concurrentRequests: '50'
}
}
}
]
}This configuration keeps 2 replicas warm during business hours for fast response times, then scales to zero on evenings and weekends. For a 0.5 vCPU container, this alone saves approximately 72% of weekly compute cost compared to running 24/7.
2. KEDA auto-scaling tuning:
KEDA scalers support cooldown periods and polling intervals that directly affect cost. The defaults are often too aggressive for cost-conscious deployments.
# KEDA ScaledObject — tuned for cost optimization
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: order-processor
spec:
scaleTargetRef:
name: order-processor
pollingInterval: 30 # check every 30s, not default 10s
cooldownPeriod: 300 # wait 5 min before scale-down
minReplicaCount: 0
maxReplicaCount: 10
triggers:
- type: azure-servicebus
metadata:
queueName: orders
namespace: sb-production
messageCount: "50"
authenticationRef:
name: sb-trigger-authTip: Increasing cooldownPeriod from 60s to 300s prevents rapid scale-up/scale-down cycles during intermittent traffic. Each unnecessary scale-up event burns at least 60 seconds of billed compute.
1. CPU/memory right-sizing with KQL:
Use Azure Monitor and Log Analytics to identify containers using far less than their allocated resources. The following KQL query surfaces containers where peak CPU usage is less than 50% of their allocation over the past 7 days.
// KQL — identify over-provisioned Container Apps
ContainerAppConsoleLogs_CL
| where TimeGenerated > ago(7d)
| summarize
AvgCPU = avg(CpuUsage_d),
MaxCPU = max(CpuUsage_d),
AvgMemoryMB = avg(MemoryUsage_d),
MaxMemoryMB = max(MemoryUsage_d)
by ContainerAppName_s, ContainerName_s
| extend
AllocatedCPU = 1.0, // adjust to your actual allocation
AllocatedMemoryMB = 2048 // adjust to your actual allocation
| extend
CPUUtilPct = round(MaxCPU / AllocatedCPU * 100, 1),
MemUtilPct = round(MaxMemoryMB / AllocatedMemoryMB * 100, 1)
| where CPUUtilPct < 50 or MemUtilPct < 50
| project
ContainerAppName_s,
ContainerName_s,
CPUUtilPct,
MemUtilPct,
RecommendedCPU = case(
MaxCPU < 0.125, 0.25,
MaxCPU < 0.375, 0.5,
MaxCPU < 0.75, 1.0,
2.0
)
| order by CPUUtilPct asc2. Dedicated workload profile consolidation:
If you have multiple dedicated workload profiles with low utilization, consolidate containers onto fewer profiles. Each dedicated profile provisions at least one node, and that node bills continuously regardless of how many containers run on it.
# List all workload profiles and their utilization
az containerapp env workload-profile list \
--resource-group rg-production \
--name my-environment \
--output table
# Move a container app to a different workload profile
az containerapp update \
--name my-api \
--resource-group rg-production \
--workload-profile-name "Consumption" # move back to consumption
# Remove unused dedicated workload profile
az containerapp env workload-profile delete \
--resource-group rg-production \
--name my-environment \
--workload-profile-name "dedicated-d4"Warning: Before removing a dedicated workload profile, verify no containers require its specific capabilities (GPU, larger memory, VNET integration). Moving to Consumption introduces cold-start latency and size limits.
Container Apps charges for HTTP requests through the ingress layer. Services that only communicate internally should use internal ingress or no ingress at all to avoid unnecessary request billing.
# Set ingress to internal-only
az containerapp ingress update \
--name background-worker \
--resource-group rg-production \
--type internal
# Disable ingress entirely for queue-driven workers
az containerapp ingress disable \
--name queue-processor \
--resource-group rg-productionContainer Apps with mounted Azure Files or managed disks often use Premium tiers by default. For logging, temporary storage, or infrequently accessed data, downgrading to Standard tiers can cut storage costs by 50-70%.
# Check current storage mounts for a container app
az containerapp show \
--name my-api \
--resource-group rg-production \
--query "properties.template.volumes"
# Update Azure Files share to use Standard tier
az storage share-rm update \
--resource-group rg-production \
--storage-account mystorageaccount \
--name app-logs \
--access-tier "TransactionOptimized" # or "Cool" for infrequent accessFor workloads with predictable, sustained usage that cannot scale to zero (e.g., production APIs with SLA requirements), Azure Savings Plans provide significant discounts over pay-as-you-go pricing.
Recommendation: Only commit to savings plans after right-sizing and implementing scale-to-zero. Committing before optimization locks in spend on resources you may not need.
The following table summarizes each optimization category, expected savings range, and implementation complexity.
| Recommendation | Savings Range | Complexity | Applies To |
|---|---|---|---|
| Scale-to-zero (minReplicas: 0) | 40-100% | Low | All idle workloads |
| CPU/memory right-sizing | 25-75% | Low | Over-provisioned containers |
| Cron-based scheduling | 50-72% | Medium | Business-hours workloads |
| Internal ingress routing | 10-30% | Low | Internal microservices |
| Workload profile consolidation | 30-60% | Medium | Dedicated profile environments |
| Autoscale threshold tuning | 15-40% | Medium | HTTP and event-driven apps |
| Storage tier optimization | 50-70% | Low | Mounted volumes |
| Savings plans (1-year) | 15-20% | Low | Sustained production workloads |
| Savings plans (3-year) | 30-40% | Low | Long-term production workloads |
Understanding the billing model is essential before optimizing. Here is how Container Apps charges across different states and configurations.
| Billing Component | How It Works | Cost Impact |
|---|---|---|
| Active billing | Charged per vCPU-second and GiB-second while replicas are running and processing requests | Primary cost driver |
| Idle billing | Reduced rate charged when replicas are running but not processing requests | Lower rate, but still billed |
| Scale-to-zero | No charge when all replicas scale to zero (Consumption plan only) | Zero cost when idle |
| Free grants | 180K vCPU-s, 360K GiB-s, 2M requests per subscription per month | Offsets initial usage |
| Dedicated profiles | Billed per provisioned node regardless of container utilization | Fixed cost per node |
| Consumption profiles | Billed per replica per second, only when active or idle (not scaled to zero) | Pay-per-use |
| HTTP requests | First 2M free, then billed per request through external ingress | Varies with traffic volume |
Yes. When a container scales from zero, the first request triggers container initialization, which typically takes 2-10 seconds depending on image size and startup logic. For latency-sensitive production APIs, use minReplicas: 1 during business hours with cron-based scheduling, and scale to zero only during known idle periods. For background workers and event-driven processors, cold start is usually acceptable.
Use Consumption for workloads with variable traffic that benefit from scale-to-zero. Use Dedicated only when you need GPU support, containers larger than 4 vCPU / 8 GiB, custom VNET integration, or when sustained utilization exceeds 60-70% of a dedicated node. If your dedicated nodes average below 50% utilization, you are likely overpaying.
Yes. A single Container Apps environment can have both Consumption and Dedicated workload profiles. This allows you to run latency-sensitive or resource-intensive workloads on Dedicated nodes while keeping lightweight microservices on Consumption with scale-to-zero. This is the recommended pattern for most production environments.
The free grant of 180,000 vCPU-seconds, 360,000 GiB-seconds, and 2 million requests is per subscription per month, shared across all Consumption-plan environments in that subscription. If you run multiple environments, the grant is consumed faster. For MSPs managing multiple tenants, consider separate subscriptions to maximize free tier coverage.
Azure Cost Management provides cost views filtered by service, but with 8-24 hour delays for EA/MCA subscriptions. For faster visibility, use Azure Monitor metrics for replica counts and request rates, combine with the pricing calculator for real-time cost estimates, and set up budget alerts at the resource group level. Spotto provides continuous cost analysis across all your Container Apps environments with recommendations surfaced automatically.
Changing CPU or memory allocation on a Container App triggers a new revision deployment. The old revision continues serving traffic until the new one is healthy. This is safe for stateless workloads. For stateful workloads, test the new allocation in a staging revision first. Always review the KQL query results from the past 7 days to ensure peak usage stays below 80% of the new allocation.
When multiple scaling rules are configured, Container Apps scales to the maximum replica count recommended by any active rule. For example, if your HTTP rule says 3 replicas and your queue rule says 5 replicas, you get 5. This means aggressive rules on any single scaler can override conservative settings on others. Review all active rules together, not in isolation.
Azure Savings Plans for Compute apply to Container Apps alongside VMs, App Service, and Azure Functions. The discount is applied automatically to eligible usage. However, savings plans only benefit sustained usage that exceeds the free grant. Right-size and implement scale-to-zero first, then commit to savings plans based on your optimized baseline.
External ingress routes through Azure's load balancer and counts toward the 2 million free request quota (then billed per-request). Internal ingress routes traffic within the Container Apps environment without hitting the external request quota. For microservices that only communicate with other containers in the same environment, switching to internal ingress eliminates those request charges entirely.
MSPs should audit each client environment independently because defaults vary by deployment method (portal, CLI, IaC). Common wins include ensuring minReplicas: 0 across all non-critical workloads, standardizing resource allocations based on workload type, and consolidating dedicated workload profiles where possible. Spotto automates this audit across all connected tenants and surfaces the highest-impact recommendations per environment.
A managed service provider supporting 12 small-to-mid-sized businesses discovered that 8 customer environments had Container Apps with minReplicas: 1 set as default during initial deployment. None of these workloads required always-on availability — they were internal tools, reporting dashboards, and webhook receivers with sporadic traffic.
minReplicas: 0 on all non-critical workloads, switched 3 services from Dedicated to Consumption profiles, configured internal ingress for 5 backend services.An enterprise running 40+ microservices on Container Apps found that their event-driven architecture was scaling aggressively due to low KEDA thresholds. Queue-based processors were spinning up 10+ replicas for bursts of only 20-30 messages.
messageCount threshold from 5 to 50, extended cooldownPeriod to 300s, right-sized 15 containers from 1 vCPU to 0.25 vCPU, implemented cron scaling for non-production environments.A SaaS company had provisioned three dedicated D4 workload profiles (4 vCPU, 16 GiB each) for isolation between their API tier, background processing tier, and admin tier. Monitoring revealed that peak combined utilization across all three profiles never exceeded 40% of a single D4 node.
Before making changes, collect baseline data across all Container Apps environments:
Prioritize changes by impact and risk. Start with the highest-impact, lowest-risk changes.
START
|
v
[Does the app have minReplicas > 0?]
| |
YES NO
| |
v v
[Is it latency-sensitive [Check resource allocation]
production API with SLA?] |
| | v
YES NO [Is CPU/memory > 2x peak usage?]
| | | |
v v YES NO
[Keep min=1, [Set min=0, | |
add cron immediate v v
scaling] savings] [Right-size [Review autoscale
allocation] thresholds]
|
v
[Are KEDA thresholds
below 50 messages?]
| |
YES NO
| |
v v
[Increase [Check ingress
threshold, configuration]
extend
cooldown]Use this decision tree to determine whether a workload belongs on Consumption or Dedicated profiles.
START
|
v
[Does the workload need GPU?]
| |
YES NO
| |
v v
[Use [Does it need > 4 vCPU or > 8 GiB memory?]
Dedicated | |
(GPU)] YES NO
| |
v v
[Use [Is sustained utilization > 60%?]
Dedicated | |
(D-series)] YES NO
| |
v v
[Use [Does it need custom VNET?]
Dedicated, | |
consider YES NO
savings | |
plan] v v
[Use [Use Consumption
Dedicated with scale-to-zero]
with VNET]Implement optimizations in waves, validating each change before proceeding.
Every optimization carries potential failure modes. Plan rollback procedures before implementing changes.
minReplicas back to 1 and add cron scheduling instead. Container Apps supports instant revision rollback via az containerapp revision activate.Rollback rule: If any optimization causes user-facing errors or SLA breaches, revert immediately and investigate. Cost optimization should never compromise reliability. Use Container Apps revision management to maintain the previous working configuration alongside the new one.
minReplicas: 0 eliminates cost for idle containers and can keep low-traffic workloads within the free grant entirely.Azure Container Apps cost optimization comes down to four principles: eliminate idle compute through scale-to-zero, right-size resources to actual usage, use the correct billing model for each workload type, and route traffic efficiently through internal ingress where possible.
The MSP case study illustrates the core problem: default configurations quietly accumulate costs that are easy to miss until the invoice arrives. An environment paying $200/month for a container that should cost $0 during idle hours is a small leak, but across 12 environments, those leaks add up to $1,800/month in waste.
The optimizations in this guide are ordered by impact and complexity. Start with scale-to-zero and resource right-sizing, which require only configuration changes and deliver the largest savings. Then implement scheduling and autoscale tuning. Save commitment-based discounts for last, after your baseline spend reflects actual optimized usage.
For MSPs and teams managing multiple Container Apps environments, continuous automated auditing is essential. Manual reviews catch point-in-time issues, but configuration drift reintroduces waste over time. Spotto provides ongoing visibility into Container Apps configurations across all connected tenants, surfacing the highest-impact recommendations automatically.
Explore our cloud cost optimization resources or review Spotto pricing to see how teams operationalize container cost control.
The challenges outlined in this article — idle container spend, workload profile sizing, scale-to-zero configuration — are the same ones our customers work through across their Azure environments. The difference is they use Spotto to surface these opportunities automatically, rather than running the analysis by hand.
Spotto continuously analyzes your Container Apps environments to identify scale-to-zero opportunities, workload profile mismatches, and ingress misconfigurations across all your tenants.
Free TrialDisclaimer: This article is provided for informational purposes only and does not constitute professional advice. Azure pricing, features, and service behavior may have changed since publication. Always validate recommendations against your specific environment and consult Microsoft's official documentation before making changes to production infrastructure. Spotto is not liable for any actions taken based on the content of this article.