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.
| Policy | Use it when |
|---|---|
| Target tracking | Nearly always. Keep a metric at a value. |
| Step scaling | Response must vary with how far off you are |
| Simple scaling | Never. Superseded by step scaling. |
| Scheduled | Load follows the clock, not a metric |
| Predictive | Daily or weekly cycles with slow-booting instances |
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.
--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.
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 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 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.
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 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.
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.
Capacity oscillating up and down usually means one of three things:
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 tableEveryone 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.
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.
| Task | Command |
|---|---|
| Target tracking | put-scaling-policy --policy-type TargetTrackingScaling |
| Step scaling | put-scaling-policy --policy-type StepScaling |
| Scheduled | put-scheduled-update-group-action --recurrence '30 7 * * MON-FRI' |
| Predictive | put-scaling-policy --policy-type PredictiveScaling |
| Set warmup | update-auto-scaling-group --default-instance-warmup 180 |
| Why did it scale? | describe-scaling-activities |
| List policies | describe-policies --auto-scaling-group-name <g> |
| Delete a policy | delete-policy --policy-name <p> |
--default-instance-warmup. Without it, booting instances cause flapping.--time-zone, which handles DST.ForecastOnly and keep target tracking alongside it.<MCQExercise exercise={{ id: 'aws-asp-quiz1', title: 'Step adjustments that never fire', description: '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?', options: [ { id: 'a', text: 'Step bounds are offsets from the alarm threshold, so these steps begin at 130% CPU and are never reached' }, { id: 'b', text: 'Step scaling requires at least four adjustments to be valid' }, { id: 'c', text: 'The alarm must use Maximum rather than Average for step scaling to work' }, { id: 'd', text: 'Step scaling only supports PercentChangeInCapacity as an adjustment type' }, ], correctId: 'a', explanation: 'MetricIntervalLowerBound and MetricIntervalUpperBound are measured relative to the alarm threshold, not as absolute metric values. With a threshold of 60, a lower bound of 70 means 130% CPU. Unreachable. The first step should start at a lower bound of 0, meaning the threshold itself, and subsequent steps add offsets from there: 0 to 20 covers 60 to 80% CPU, 20 to 40 covers 80 to 100%, and so on. Leave the final step's upper bound unset so there is no gap above it.', wrongExplanations: { b: 'A single step adjustment is valid. The count is not the problem.', c: 'Average is a normal and usually correct statistic for CPU-based scaling alarms.', d: 'ChangeInCapacity, ExactCapacity and PercentChangeInCapacity are all supported.', }, }} />
<MCQExercise exercise={{ id: 'aws-asp-quiz2', title: 'Capacity that oscillates', description: '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?', options: [ { id: 'a', text: 'Set --default-instance-warmup to cover boot time, so booting instances reporting low CPU do not drag the average down and trigger scale-in' }, { id: 'b', text: 'Switch to simple scaling, whose cooldown prevents repeated activity' }, { id: 'c', text: 'Raise max-size so the group has room to stabilise' }, { id: 'd', text: 'Replace target tracking with a scheduled action every fifteen minutes' }, ], correctId: 'a', explanation: 'A newly launched instance reports very low CPU for the first few minutes while it boots. Included in the group average, it pulls the figure below target, the policy concludes it has too much capacity, and it scales in. Then load rises again and the cycle repeats. Instance warmup excludes those instances from metric aggregation until they are genuinely serving. A 25% target is also close enough to idle that ordinary variation crosses it constantly, so raising it to 40 to 60% gives a more stable band once warmup is set.', wrongExplanations: { b: 'Simple scaling is deprecated and its cooldown would suppress needed scaling as well as unwanted scaling.', c: 'The group is oscillating, not hitting a ceiling. More headroom does not stop the cycle.', d: 'Scheduled actions handle clock-driven load, and would ignore the actual metric entirely.', }, }} />
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.
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.
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.
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.
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.
Learn how CloudFront caching works: origin access control for S3, the cache key behind your hit rate, TTL header rules, invalidation costs and error pages.
Create a CloudFront distribution step by step. Learn behavior precedence, multiple origins, origin groups for failover and Functions vs Lambda@Edge.
Serve CloudFront on your own domain over HTTPS: request the ACM certificate in us-east-1, validate with DNS, add the alias record and pick a security policy.