In Azure Container Apps there is a parameter that looks completely harmless and which, if set to the wrong value, can cost more than the entire rest of the application combined. It is called minReplicas, has a default value of 0, and almost everyone changes it to 1 within the first week.
The reason is always the same and perfectly reasonable: I don't want a cold start on the first request. However, the price of that decision is rarely calculated.
In the previous article, we did the calculation for Azure Functions. Now we move on to containers, where the billing model is different — and where waste is easier to produce because it is better hidden.
How Container Apps are actually billed
On the Consumption profile, you pay per second for the vCPU and memory allocated to your replicas. The part many miss is that there are two different rates for exactly the same vCPU:
| Status | When it applies | Indicative rate (East US) |
|---|---|---|
| Active | The replica is starting or processing at least one request | ~$0.086 / vCPU-hour |
| Idle | The replica is running but has no traffic | ~$0.011 / vCPU-hour |
| Zero | The application has scaled down to 0 replicas | $0 |
There is also a monthly free tier per subscription — around 180,000 vCPU-seconds, 360,000 GiB-seconds, and 2 million requests — which fully covers small applications.
The idle rate seems negligible. Let's multiply it.
The arithmetic of minReplicas: 1
A service with 0.5 vCPU and 1 GiB, with minReplicas: 1, runs 730 hours per month. If it has real traffic 2 hours per day, the remaining ~670 hours are billed at the idle rate.
Per service, per month, we are talking about a few dollars. Nothing alarming — and that's exactly why it goes unnoticed.
Now multiply by reality: six microservices, three environments (dev, staging, prod), each with minReplicas: 1 reflexively set at creation. Eighteen replicas running permanently to serve, in the non-prod environments, traffic of approximately zero.
The idle cost is not high per unit. It is high because it multiplies by the number of services, by the number of environments, and by 730 hours per month — and none of these three multipliers appears in the initial decision.
The practical rule I apply: in dev and staging, minReplicas is 0. No exceptions. A cold start of a few seconds in staging does not bother anyone. In production, the decision is made per service, based on real traffic — not based on fear.
KEDA: scale based on what matters, not on CPU
Container Apps uses KEDA for scaling, and this is more important than it sounds. The classic autoscaling model reacts to CPU and memory — that is, to symptoms. KEDA reacts to cause: queue length, number of unprocessed messages, rows in a table.
For a worker consuming from a queue, the difference is fundamental. CPU-based scaling is a delayed indicator — the queue grows for a minute before the CPU reacts. Scaling based on queue length is immediate.
resource app 'Microsoft.App/containerApps@2024-03-01' = {
name: 'order-processor'
properties: {
configuration: { ... }
template: {
scale: {
minReplicas: 0
maxReplicas: 10
rules: [
{
name: 'queue-depth'
custom: {
type: 'azure-servicebus'
metadata: {
queueName: 'orders'
messageCount: '20'
}
auth: [ ... ]
}
}
]
}
}
}
}
Here messageCount: 20 means: start a new replica for every 20 messages waiting. This parameter is where the second major waste in Container Apps occurs.
Too aggressive thresholds
If you set messageCount: 1, a peak of 30 messages — which would have been processed in five seconds by a single replica — triggers the start of ten replicas. Each starts, each is billed at the active rate during startup, each processes two messages and then idles until the cooldown period expires.
You paid ten times the startup cost to save three seconds. And worse, you created ten simultaneous connections to the database for a trivial burst.
The correct threshold is calculated inversely: how long does it take to process a message and what is the acceptable latency? If a message is processed in 200 ms and you accept 10 seconds of delay, one replica can handle 50 messages. That is the threshold, not 1.
App Service: autoscale based on rules, not hope
App Service works on a different model — reserved capacity, with horizontal scaling between instances of the same tier. It does not scale to zero. Here optimization has three layers.
1. The correct tier, verified with data
The most common waste in App Service is a tier chosen at launch "to be safe" and not touched for two years. Azure Advisor explicitly signals this, but the report does not open itself. A plan with 12% constant CPU is an oversized plan, no matter how reassuring it looks.
2. Scale-out with symmetric rules
The classic mistake is to configure the scale-up rule and forget the scale-down one. The application climbs to 5 instances at an 11 o'clock peak and stays there until restart.
Each scale-out rule needs its scale-in pair, with asymmetric thresholds, to avoid oscillation: scale up above 70% CPU, scale down below 30%. If the thresholds are too close, the system enters a continuous start-stop cycle — which, obviously, is billed.
3. Scheduled scaling for predictable traffic
If your application is a business tool used between 8 AM and 6 PM, Monday to Friday, reactive scaling is the wrong tool. The reaction always comes with delay — users feel the slowness before the new instance is ready.
Schedule-based rules solve both problems: increased capacity in the morning, reduced in the evening and on weekends. It is cheaper and faster than reactive autoscale because you do not wait for the metric.
Non-production environments: the most profitable hour of work
I mentioned this in the first article, but it is worth repeating with numbers.
Dev and staging are realistically used 40–50 hours per week. They are billed for 168. Over 70% of the cost of non-prod environments is consumed while no one is looking at them — nights, weekends, holidays.
Solutions, in order of effort:
- Container Apps:
minReplicas: 0. That's it. The cost becomes almost zero automatically. - App Service: scheduled autoscale rule that lowers to one instance of the minimum tier outside business hours.
- Anything else (VMs, databases): an Automation Runbook or a GitHub Actions job that stops resources at 8:00 PM and starts them at 7:30 AM on working days.
The last point is about an hour of work. Its annual return is, percentage-wise, better than almost anything else you do in infrastructure.
Configuration checklist
What I check every time I create or inherit a containerized service:
- Is
minReplicas0 in dev and staging? If not, why? - In production, does the value reflect real measured traffic or a six-month-old assumption?
- Does
maxReplicashave a reasonable limit? It is the safety net that stops an infinite loop from generating a thousand-dollar bill. - Is the KEDA threshold calculated from processing time and acceptable latency, or is it a round number chosen intuitively?
- Does each scale-out rule have its scale-in pair, with asymmetric thresholds?
- Is the traffic predictable? Then I use scheduled scaling, not reactive.
- Do test resources stop themselves outside business hours?
Conclusion
Auto-scaling is not a feature you activate. It is a set of parameters you calibrate — and the default values are optimized not to annoy you, not to cost you little.
The two most expensive numbers in a Container Apps configuration are minReplicas and the KEDA scaling threshold. Both are set in thirty seconds and remain unchanged for years. They are worth the ten minutes of thought.
In the next article, we move from configuration to measurement: Azure Cost Management, tagging, budgets, anomaly detection, and how to build a monthly cost audit that takes twenty minutes and actually gets done.
Note: the indicated rates are indicative, for the East US region, at the time of writing. Always check current prices for your region.