Over-provisioned Auto Scaling groups: the basics
Why a 'safe' minimum capacity quietly costs the most
An Auto Scaling group (ASG) maintains a fleet of identical EC2 instances between a min, desired, and max capacity. The whole point is elasticity: add instances when load rises, remove them when it falls. But the most common waste pattern isn't a broken scaling policy; it's a static desired (or a min) set high once "for safety" and never revisited. If peak real demand needs 2 instances and someone set min=6, four instances run around the clock doing nothing, billed at the full hourly rate every hour of every month.
The math is unforgiving because it's pure multiplication. Four idle m5.large instances at roughly $0.096/hour are about $0.38/hour, $9.21/day, $280/month, for a group that the workload never actually needed above 2. Reserved Instances and Savings Plans don't rescue you here; a commitment against capacity you don't need is still waste, just pre-paid waste. The fix is to stop pinning capacity to a number a human guessed and instead let it track a signal the workload actually produces.
It's flagged because over-provisioning hides in plain sight. The ASG is healthy, alarms are green, dashboards look fine. That's exactly the problem. Nobody gets paged for an idle instance. The group was sized for a launch-day spike that happened once in 2024, the min was bumped during an incident and never lowered, and now the baseline is permanently inflated. Cost Optimization Hub surfaces these as over-provisioned Ec2AutoScalingGroup findings precisely because they never trip an operational signal.
In this lesson you'll learn how to tell a genuinely-elastic Auto Scaling group from one that's pinned to an inflated baseline, how to set min/desired/max so the group floats instead of squatting, and how to replace a static desired count with a dynamic scaling policy tied to a real signal, average CPU, or better, ALBRequestCountPerTarget. You'll see target-tracking, step, scheduled, and predictive scaling and when each fits, how warm pools let you safely lower the baseline without paying a latency penalty, and why right-sizing the group (the count) is a separate exercise from right-sizing the instance type (the box), you should do both. You'll see the AWS CLI to audit a group and apply the fix, plus the availability guardrail that stops you scaling in so hard you breach an AZ.
The incident that never scaled back down
Auto Scaling will happily honour a min that's larger than your desired, it simply scales up to meet the floor and stays there. A retail platform team once raised an ASG's min from 3 to 12 during a Black Friday incident to ride out the traffic, fully intending to lower it the following Monday. The runbook step to revert was never written down. Eighteen months later a Cost Optimization Hub audit flagged the group as over-provisioned: nine of those twelve instances had been idle every single day since, at a cumulative cost north of $90,000. The fix was a one-line update-auto-scaling-group --min-size 3, the hard part was that no alarm had ever fired, because over-provisioning is invisible to every signal except the bill.
Scaling in an over-provisioned ASG in action
Marcus runs the platform team at a logistics company. Cost Optimization Hub flags an ASG behind their tracking API as an over-provisioned Ec2AutoScalingGroup: min=6, desired=6, max=10, six c5.xlarge instances running 24/7 at about $0.17/hour each, roughly $735/month for the group.
He pulls 14 days of aggregate CloudWatch metrics for the group. Average CPU across the fleet sits at 9%, peaking to 22% during the morning dispatch window. The ALB shows RequestCountPerTarget averaging 40 req/min with a clean daily curve, busy 7am-7pm, near-idle overnight. Two instances would carry the average comfortably; even the peak is well inside three.
Marcus doesn't just slam desired to 2, a static low number is as wrong as a static high one. He lowers min to 2, sets max to 6 for genuine spike headroom, and attaches a target-tracking policy on ALBRequestCountPerTarget so the group floats with real demand. He keeps a warm pool of one stopped instance so scale-out is seconds, not minutes, which lets him trust the lower floor. Projected saving: about $480/month, the fleet now flexing between 2 and 4 instead of squatting at 6.
First, inspect the group's capacity settings and how many instances it's actually running.
Capacity settings: min equal to desired and a static count is the over-provisioning tell.
Lower the floor for real headroom, then attach a target-tracking policy so capacity floats with ALB demand instead of a static number.
Floor dropped to 2 with max 6 for spikes; target tracking now sizes the fleet to 60 requests/target.
Auto Scaling capacity under the hooddeep dive
An ASG continuously reconciles toward DesiredCapacity, clamped between MinSize and MaxSize. With no scaling policy, desired is whatever a human last set it to, so the group is effectively a static fleet wearing an elastic label. min is a hard floor the group will never go below even if a policy wants to, that's why a min raised during an incident is so dangerous: scaling-in policies physically cannot reclaim those instances until someone lowers min by hand. Pricing is just instance-hours: a c5.xlarge is about $0.17/hour, so each instance you shave off the floor is roughly $122/month back, immediately, at the next billing hour.
Dynamic scaling comes in three flavours and you pick by the shape of demand. Target tracking is the default choice: you name a metric (ASGAverageCPUUtilization, or far better for web tiers, ALBRequestCountPerTarget) and a target value, and Auto Scaling manages the alarms and adjustments to hold it, like a thermostat. Step scaling adds or removes a defined number of instances per alarm breach band, useful when you want bigger jumps under heavier breach. Scheduled scaling sets capacity by clock for known patterns (raise min at 7am weekdays, drop it at 8pm) and is the cleanest fix for predictable daily or weekly cycles. Predictive scaling uses machine learning on up to 14 days of history to provision ahead of recurring cyclical peaks, eliminating the cold-start lag at the start of a known surge.
The reason teams over-provision in the first place is scale-out latency: launching and bootstrapping a fresh instance can take minutes, so a nervous engineer raises the floor to mask it. Warm pools fix the root cause: they keep pre-initialised instances in a Stopped (you pay only EBS) or Hibernated state, ready to join the group in seconds, so you can safely lower the baseline. Critically, scaling the group (how many instances) is orthogonal to right-sizing the instance type (how big each one is): a group can be perfectly elastic and still run on c5.4xlarge boxes that should be c5.large. Do both, Cost Optimization Hub reports them as separate findings, an over-provisioned Ec2AutoScalingGroup versus an over-provisioned Ec2Instance. And never scale in so aggressively that a single-AZ failure drops you below capacity: keep at least N+1 across your AZs.
# Pull 14 days of aggregate CPU for the whole group to judge the true baseline.
aws cloudwatch get-metric-statistics \
--namespace AWS/EC2 --metric-name CPUUtilization \
--dimensions Name=AutoScalingGroupName,Value=tracking-api-asg \
--start-time $(date -u -d '14 days ago' +%FT%TZ) \
--end-time $(date -u +%FT%TZ) \
--period 3600 --statistics Average Maximum
# For predictable daily peaks, scheduled scaling beats a static floor.
aws autoscaling put-scheduled-update-group-action \
--auto-scaling-group-name tracking-api-asg \
--scheduled-action-name business-hours-up \
--recurrence '0 7 * * MON-FRI' \
--min-size 3 --desired-capacity 3 --max-size 6
aws autoscaling put-scheduled-update-group-action \
--auto-scaling-group-name tracking-api-asg \
--scheduled-action-name overnight-down \
--recurrence '0 20 * * *' \
--min-size 1 --desired-capacity 1 --max-size 6 What is the impact of over-provisioned Auto Scaling groups?
The direct cost is idle instance-hours on the inflated baseline. A group pinned four instances higher than it needs, on c5.xlarge at ~$0.17/hour, is about $490/month per group of pure waste. On m5.large at ~$0.096/hour it's $280/month. Across a few dozen long-lived ASGs in a mature estate, each over-provisioned by two to six instances because a min was bumped once and forgotten, that's commonly $10 to 40k/month, billed every hour regardless of whether load ever approaches the floor.
There's a compounding second-order cost: over-provisioning trains teams to stop trusting elasticity. Once a group has been pinned high "and it's fine," the next nervous engineer does the same, and the muscle memory becomes "add a floor when worried" instead of "add a scaling policy." The estate slowly converts from elastic to static, which is precisely the rigidity you moved off-premises to escape, except you're now paying cloud per-hour rates for it.
Commitments make it worse. A Savings Plan or Reserved Instance bought against an inflated baseline locks the overspend in for one to three years; right-sizing the group afterwards strands part of the commitment against capacity you no longer run. The correct order is always right-size first, commit second, commit to your true elastic baseline, not your nervous one. An over-provisioned ASG signed into a three-year commitment is the most expensive version of this mistake.
Finally, the inverse failure is real and worth naming: scale in too hard and you breach availability. If you drop a floor so low that a single-AZ outage takes you below the capacity needed to serve traffic, you've traded a cost problem for an incident. The discipline isn't "minimise instances"; it's "size the floor to the true baseline plus N+1 AZ headroom, and let a policy handle everything above it." Done right, scaling in improves both the bill and the architecture's honesty about what it actually needs.
How do you scale in an over-provisioned ASG safely?
Scaling in is a four-step loop: measure the true baseline and peak, reset min/desired/max to that reality, replace the static count with a policy tied to a real signal, and protect availability so you never trade cost for an incident.
1. Measure the true baseline and peak before touching anything
Pull at least 14 days of aggregate CloudWatch metrics for the whole group, average and maximum CPU, and the ALB's RequestCountPerTarget if it's behind a load balancer. The baseline is the floor the workload genuinely needs; the peak (plus margin) is your max. A group averaging 9% CPU and peaking to 22% across six instances was sized for a peak that needs two or three, not six. Note the daily and weekly shape too, a clean diurnal curve points you toward scheduled scaling, a noisy one toward target tracking.
2. Reset min/desired/max to the measured reality
Lower min to the true baseline (this is the line item that actually frees the spend, a scaling policy can never reclaim instances held below by min), set desired to the same, and set max generously above peak so a real surge is never throttled. Cutting max saves nothing; cutting the floor saves everything. Do this in one update-auto-scaling-group call so the group settles to the new shape immediately at the next reconcile.
3. Replace the static count with a dynamic scaling policy
A static desired is a guess that's wrong the moment traffic changes. Attach target tracking on ALBRequestCountPerTarget for request-driven web tiers (the most honest signal), or ASGAverageCPUUtilization for compute-bound work. Use step scaling when you want larger jumps under heavier breach, scheduled scaling for predictable daily/weekly cycles, and predictive scaling for recurring cyclical peaks where cold-start lag would otherwise hurt. To lower the baseline without paying a scale-out latency penalty, attach a warm pool so pre-initialised instances join in seconds, that's what makes a low floor safe.
4. Protect availability and right-size the box separately
Never scale in so far that losing one AZ drops you below serving capacity, keep at least N+1 across your AZs and let the ASG balance instances across them. Set sensible cooldowns and instance-warmup so the group doesn't thrash. And remember this is only half the job: scaling the group (count) is independent of right-sizing the instance type (size). After the group is elastic, check Cost Optimization Hub for an over-provisioned Ec2Instance finding on the same fleet and do that too, an elastic group of oversized boxes is still overspending.
# Reset capacity to the measured baseline, then attach target tracking on real demand.
ASG=tracking-api-asg
aws autoscaling update-auto-scaling-group \
--auto-scaling-group-name $ASG \
--min-size 2 --desired-capacity 2 --max-size 6 \
--default-cooldown 120
aws autoscaling put-scaling-policy \
--auto-scaling-group-name $ASG \
--policy-name alb-request-tracking \
--policy-type TargetTrackingScaling \
--estimated-instance-warmup 90 \
--target-tracking-configuration '{
"PredefinedMetricSpecification": {
"PredefinedMetricType": "ALBRequestCountPerTarget",
"ResourceLabel": "app/tracking-alb/abc123/targetgroup/tracking-tg/def456"
},
"TargetValue": 60.0
}'
# Optional: warm pool so the lower floor scales out in seconds, not minutes.
aws autoscaling put-warm-pool \
--auto-scaling-group-name $ASG \
--pool-state Stopped --min-size 1 --max-group-prepared-capacity 4 Quick quiz
Question 1 of 5An ASG behind an ALB has min=6, desired=6, max=10 and no scaling policy. Fourteen days of metrics show average CPU at 9%, peaking to 22%, with a clean daily curve. What's the right next move?
You scored
0 / 5
Keep learning
Dig deeper into Auto Scaling capacity, dynamic scaling policies, and the tooling around right-sizing groups.
- AWS Auto Scaling: dynamic scaling policies How target tracking, step, and simple scaling policies size a group to a live metric like CPU or ALBRequestCountPerTarget.
- AWS Auto Scaling: predictive scaling and warm pools Provisioning ahead of cyclical peaks, and keeping pre-initialised instances ready so a lower baseline scales out fast.
- AWS Cost Optimization Hub Managed AWS service that surfaces over-provisioned Ec2AutoScalingGroup findings alongside instance right-sizing recommendations.
- FinOps Foundation: Usage Optimization How matching capacity to demand fits the broader FinOps lifecycle and operating model.
You've completed Scale in over-provisioned Auto Scaling groups. You now know why a static, inflated min capacity is the most common ASG waste pattern, how to reset min/desired/max to a measured baseline, how to replace a static count with target-tracking, step, scheduled, or predictive scaling tied to a real signal, and how warm pools let you lower the floor safely without a latency penalty. You also know to right-size the group and the instance type separately, and to keep N+1 AZ headroom so you never trade cost for an incident. Next time Cost Optimization Hub flags an over-provisioned Ec2AutoScalingGroup, you'll have a defensible path from flagged to fixed.
Back to the library