AWS Auto Scaling Policies

By Vishvesh Patel · DevOps EngineerUpdated September 16, 2026

Target tracking, step, scheduled and predictive scaling compared, which to reach for, how warmup prevents flapping, and why scale-in goes wrong.

Auto Scaling Policies

An Auto Scaling group maintains a desired capacity. A scaling policy is what changes that number. There are five kinds, and the honest summary is that target tracking handles almost everything. The others exist for specific situations.

PolicyUse it when
Target trackingNearly always. Keep a metric at a value.
Step scalingResponse must vary with how far off you are
Simple scalingNever. Superseded by step scaling.
ScheduledLoad follows the clock, not a metric
PredictiveDaily or weekly cycles with slow-booting instances

Target tracking

You name a metric and a target value, and AWS manages the alarms, the arithmetic and the scale-in for you:

aws autoscaling put-scaling-policy \
  --auto-scaling-group-name web-asg \
  --policy-name cpu-50 \
  --policy-type TargetTrackingScaling \
  --target-tracking-configuration '{
    "TargetValue": 50.0,
    "PredefinedMetricSpecification": {"PredefinedMetricType": "ASGAverageCPUUtilization"}
  }'

It creates a pair of CloudWatch alarms behind the scenes and adjusts capacity to hold the metric near the target, scaling out aggressively and in conservatively, which is the behaviour you want and is tedious to implement by hand with step scaling.

For a web tier, requests per instance is usually the better signal than CPU:

--target-tracking-configuration '{
  "TargetValue": 1000.0,
  "PredefinedMetricSpecification": {
    "PredefinedMetricType": "ALBRequestCountPerTarget",
    "ResourceLabel": "app/web-alb/50dc6c495c0c9188/targetgroup/web-tg/73e2d6bc24d8a067"
  }
}'

That ResourceLabel format is fiddly and a frequent source of errors. It is the ALB and target group portions of their ARNs joined by a /. Read them from describe-target-groups rather than typing them.

Disable scale-in when you need to

--target-tracking-configuration '{
  "TargetValue": 50.0,
  "DisableScaleIn": true,
  "PredefinedMetricSpecification": {"PredefinedMetricType": "ASGAverageCPUUtilization"}
}'

Useful when you want one policy adding capacity and something else, a scheduled action, or a more cautious step policy, deciding when to remove it.

Several policies together

Attaching more than one target tracking policy is supported and sensible. The group takes the largest capacity any policy asks for when scaling out, and scales in only when all of them agree. So a CPU policy and a request-count policy together mean either can add capacity, and neither can remove it alone.

Step scaling

Step scaling varies the response by how far the metric has breached, which target tracking cannot express:

aws cloudwatch put-metric-alarm \
  --alarm-name web-cpu-high \
  --metric-name CPUUtilization --namespace AWS/EC2 \
  --dimensions Name=AutoScalingGroupName,Value=web-asg \
  --statistic Average --period 60 --evaluation-periods 2 \
  --threshold 60 --comparison-operator GreaterThanThreshold

aws autoscaling put-scaling-policy \
  --auto-scaling-group-name web-asg \
  --policy-name step-out --policy-type StepScaling \
  --adjustment-type ChangeInCapacity \
  --metric-aggregation-type Average \
  --estimated-instance-warmup 180 \
  --step-adjustments \
    'MetricIntervalLowerBound=0,MetricIntervalUpperBound=20,ScalingAdjustment=1' \
    'MetricIntervalLowerBound=20,MetricIntervalUpperBound=40,ScalingAdjustment=3' \
    'MetricIntervalLowerBound=40,ScalingAdjustment=6'

The bounds are relative to the alarm threshold, not absolute metric values. With a threshold of 60, the first step covers 60 to 80% CPU and adds one instance; the last covers 100% and upwards and adds six. Reading them as absolute values is the usual mistake and produces steps that never fire.

Leave MetricIntervalUpperBound off the last step and MetricIntervalLowerBound off the first, or there is a gap where nothing happens.

You must create and attach the alarm yourself, which is the main cost of step scaling over target tracking.

Simple scaling

Simple scaling applies one adjustment, then blocks all scaling for a cooldown period, even if the metric keeps climbing. That single-threaded behaviour is why step scaling replaced it, and there is no remaining reason to choose it.

Scheduled scaling

When load follows the clock, there is no reason to wait for a metric to notice:

aws autoscaling put-scheduled-update-group-action \
  --auto-scaling-group-name web-asg \
  --scheduled-action-name weekday-morning \
  --recurrence '30 7 * * MON-FRI' \
  --min-size 6 --desired-capacity 8 --max-size 20 \
  --time-zone 'Europe/London'
aws autoscaling put-scheduled-update-group-action \
  --auto-scaling-group-name web-asg \
  --scheduled-action-name overnight \
  --recurrence '0 20 * * *' \
  --min-size 2 --desired-capacity 2 --max-size 20 \
  --time-zone 'Europe/London'

Two details matter. The recurrence is standard five-field cron, unlike EventBridge's six fields. And --time-zone handles daylight saving properly, so a schedule set in local time does not drift by an hour twice a year, without it, times are UTC.

Scheduled actions raise the floor rather than replacing metric-based policies. Combining a scheduled minimum with target tracking is the standard pattern: guaranteed capacity before the rush, and metric-driven scaling beyond it.

Predictive scaling

Predictive scaling reads the last two weeks of history, forecasts the next 48 hours, and provisions ahead of demand:

aws autoscaling put-scaling-policy \
  --auto-scaling-group-name web-asg \
  --policy-name predictive --policy-type PredictiveScaling \
  --predictive-scaling-configuration '{
    "MetricSpecifications": [{
      "TargetValue": 50.0,
      "PredefinedMetricPairSpecification": {"PredefinedMetricType": "ASGCPUUtilization"}
    }],
    "Mode": "ForecastOnly",
    "SchedulingBufferTime": 300
  }'

Start in ForecastOnly. It produces forecasts without acting, so you can compare predictions against what actually happened before letting it provision. Switch to ForecastAndScale once the forecast looks right.

It needs at least 24 hours of data, works only on cyclical patterns, and is genuinely valuable when instances take several minutes to boot. Reactive scaling is always late by however long a launch takes. Always keep target tracking alongside it to handle the unexpected.

Cooldowns and warmup

The settings behind most "it scaled too much" complaints.

Cooldown is a period after a scaling activity during which simple scaling does nothing. It does not apply to target tracking or step scaling, so tuning it on a modern setup achieves nothing.

Instance warmup is what matters. A newly launched instance reports low CPU while booting, which drags the group average down and can trigger a scale-in immediately after scaling out. Warmup excludes it from metrics until it is ready:

aws autoscaling update-auto-scaling-group \
  --auto-scaling-group-name web-asg --default-instance-warmup 180

--default-instance-warmup on the group is the current recommendation and covers every policy. Set it to real boot-to-ready time.

Flapping

Capacity oscillating up and down usually means one of three things:

  • No instance warmup, so booting instances skew the average. Fix this first.
  • Target too close to idle. A 20% CPU target means small variations cross it constantly; 40 to 60% is a more stable band.
  • A metric that is too spiky. Increase the evaluation period, or scale on something smoother than raw CPU.

describe-scaling-activities shows each decision and its cause, which is where to look rather than guessing:

aws autoscaling describe-scaling-activities \
  --auto-scaling-group-name web-asg --max-items 10 \
  --query 'Activities[].[StartTime,StatusCode,Cause]' --output table

Scale-in is the part that goes wrong

Everyone tests scaling out. Scale-in is where the incidents come from: connections cut mid-request, jobs abandoned half-finished, a cache that has to warm from cold again.

Three safeguards. Set a deregistration delay slightly above your longest request so the load balancer drains connections. Use a lifecycle hook on termination to finish in-flight work. And keep min-size at genuine minimum safe capacity, not at one.

Common errors

ValidationError: Invalid ResourceLabel format

The ALBRequestCountPerTarget label is app/<alb-name>/<alb-id>/targetgroup/<tg-name>/<tg-id>. Build it from the ARNs rather than by hand.

ValidationError: Step adjustments overlap

Two steps cover the same interval. Bounds must be contiguous and non-overlapping.

Alarm in ALARM, no scaling activity. Desired capacity is already at max-size, or a service quota is blocking launches. describe-scaling-activities names it.

Quick reference

TaskCommand
Target trackingput-scaling-policy --policy-type TargetTrackingScaling
Step scalingput-scaling-policy --policy-type StepScaling
Scheduledput-scheduled-update-group-action --recurrence '30 7 * * MON-FRI'
Predictiveput-scaling-policy --policy-type PredictiveScaling
Set warmupupdate-auto-scaling-group --default-instance-warmup 180
Why did it scale?describe-scaling-activities
List policiesdescribe-policies --auto-scaling-group-name <g>
Delete a policydelete-policy --policy-name <p>

Key takeaways

  • Use target tracking. It manages alarms, arithmetic and asymmetric scale-in for you.
  • Step scaling bounds are relative to the alarm threshold, not absolute metric values.
  • Simple scaling is obsolete. Step scaling replaced it.
  • Several target tracking policies coexist safely: largest wins out, unanimous to scale in.
  • Set --default-instance-warmup. Without it, booting instances cause flapping.
  • Scheduled actions use five-field cron and accept --time-zone, which handles DST.
  • Start predictive scaling in ForecastOnly and keep target tracking alongside it.
  • Test scale-in. Deregistration delay and lifecycle hooks are what make it safe.

Check your understanding

Exercise

Step adjustments that never fire

A step scaling policy has an alarm threshold of 60% CPU and step adjustments with bounds 70 to 80, 80 to 90, and 90 upwards. Nothing scales even at 95% CPU. Why?

Exercise

Capacity that oscillates

A target tracking policy on ASGAverageCPUUtilization with a target of 25% is adding and removing instances every few minutes. Instances take about three minutes to boot. What is the first thing to fix?


Frequently Asked Questions

What is the difference between target tracking and step scaling?

Target tracking takes a metric and a target value and manages everything else. It creates the CloudWatch alarms, works out how much capacity to add or remove, and deliberately scales out faster than it scales in. Step scaling requires you to create the alarm and define adjustments that vary with how far the metric has breached, which is more work but lets you respond more aggressively to a large breach than a small one. Start with target tracking; reach for step scaling only when that graduated response genuinely matters.

Which metric should a scaling policy use?

Whichever tracks arriving work. ASGAverageCPUUtilization suits compute-bound services. ALBRequestCountPerTarget is usually better for a web tier, because requests per instance maps directly to load whether or not it is CPU-heavy. For a queue consumer, use backlog per instance, since CPU only rises once the fleet is already behind. Avoid targets close to idle, around 20% means normal variation crosses the threshold constantly and the group flaps.

Why does my Auto Scaling group keep flapping up and down?

Most often no instance warmup configured. A booting instance reports near-zero CPU, which lowers the group average below target and triggers a scale-in immediately after scaling out. Set --default-instance-warmup to your real boot-to-ready time. The other two causes are a target value close to idle, where ordinary noise crosses it repeatedly, and a metric too spiky to scale on. Both fixed by raising the target into the 40 to 60% range or lengthening the evaluation period.

Can I attach more than one scaling policy to a group?

Yes, and it is a sensible pattern. When several target tracking policies apply, the group scales out to the largest capacity any of them requests, and scales in only when all of them agree it is safe. So a CPU policy and a request-count policy together mean either can add capacity while neither can remove it alone. Combining a scheduled action that raises the floor with target tracking above it is also standard.

Do cooldown periods apply to target tracking policies?

No. Cooldown is a simple scaling concept, and since simple scaling is obsolete, tuning cooldown on a modern configuration achieves nothing. The setting that matters is instance warmup, which excludes newly launched instances from metric aggregation until they are actually serving traffic. --default-instance-warmup on the group is the current recommendation and applies to every policy attached to it.

Is predictive scaling worth using?

It is, on workloads with clear daily or weekly cycles and instances slow enough to boot that reactive scaling is always late. It forecasts from the last fortnight of history and provisions ahead of expected demand, which reactive policies cannot do by definition. Start in ForecastOnly mode and compare its predictions against reality for a week or two before enabling ForecastAndScale, and always keep a target tracking policy alongside it to handle load the forecast did not anticipate.

Continue Learning

Explore Related Topics

When it breaks

Related Resources