Filter concepts by levelShowing all levels.

AWS · Section 14

Auto Scaling

Level
intermediate
Read
28 min
Concepts
4

An Auto Scaling Group keeps a fleet bounded and self-healing — but that only works if the application itself is designed to be replaced at any time. This section covers the core ASG vocabulary (min/desired/max, launch templates, lifecycle hooks), the scaling policy types and when each fits, how Auto Scaling integrates with a load balancer's own health checks, and why designing for instance replacement — not permanence — is what makes all of it safe.

This section

What is true here

  1. min ≤ desired ≤ max bounds and targets an Auto Scaling Group's size; a launch template defines what every new instance runs.
  2. Target tracking reacts continuously to a metric; step scaling maps alarm severities to responses; scheduled and predictive scaling get ahead of known patterns.
  3. Attaching a target group automates registration and deregistration — but only the ELB health check type lets Auto Scaling react to application-level failures.
  4. Any instance can be replaced at any time (health check, scale-in, refresh, Spot interruption) — applications must externalize state and handle shutdown gracefully within the deregistration delay window.

What you will be able to do

  • Explain the relationship between an ASG's min, desired, and max capacity settings
  • Choose the right combination of scaling policies for a workload's traffic pattern
  • Configure an ASG's health check to catch application-level failures, not just infrastructure ones
  • Design an application to survive instance replacement without losing state or dropping in-flight work
From a bounded group to a replacement-tolerant application
sized bylaunches/terminatestriggerrequires

ASG: min ≤ desired ≤ max

Scaling policy adjusts desired

Load balancer registers/deregisters

App designed for replacement

externalized state, graceful shutdown

  • ASG: min ≤ desired ≤ max
    • leads to Scaling policy adjusts desired (sized by)
  • Scaling policy adjusts desired
    • leads to Load balancer registers/deregisters (launches/terminates trigger)
  • Load balancer registers/deregisters
    • leads to App designed for replacement (requires)
  • App designed for replacement — externalized state, graceful shutdown

Auto Scaling

The core ASG vocabulary, scaling policy types, load balancer integration, and designing applications for instance replacement.

Auto Scaling Group vocabulary

coreintermediate

An Auto Scaling Group keeps a fleet of instances at a target size — never below min, never above max, and always trying to reach desired — launching them from a launch template, and replacing any instance that fails a health check.

Think of it as

A thermostat for instance count: min and max are the hard floor and ceiling, desired is the target temperature, and the health check is what tells it a specific unit stopped working and needs swapping.

bash
aws autoscaling create-auto-scaling-group --auto-scaling-group-name app-asg \
  --launch-template LaunchTemplateName=app-lt --min-size 2 --max-size 10 --desired-capacity 4 \
  --vpc-zone-identifier "subnet-0a,subnet-0b"

What we're doing: See how a lifecycle hook delays termination just long enough to drain in-flight requests.

terminating-hook.shbash
aws autoscaling put-lifecycle-hook --lifecycle-hook-name drain-connections \
  --auto-scaling-group-name app-asg --lifecycle-transition autoscaling:EC2_INSTANCE_TERMINATING \
  --heartbeat-timeout 120
1
This hook fires before the instance is actually terminated, not after.
3
The instance has up to 120 seconds in a "Terminating:Wait" state — enough time for in-flight requests to finish before the instance is actually removed.

Why this works: Without a lifecycle hook, Auto Scaling can terminate an instance mid-request — the hook creates a window for the instance (or an external process watching for the event) to finish current work and deregister cleanly first.

Setting desired capacity below min, or above max

Wrong

bash
aws autoscaling update-auto-scaling-group --min-size 4 --max-size 10 --desired-capacity 2

Better

bash
aws autoscaling update-auto-scaling-group --min-size 4 --max-size 10 --desired-capacity 6

What you see: The update is rejected, or desired capacity is silently clamped to the nearest valid bound.

Why: min, max, and desired have a fixed relationship (min ≤ desired ≤ max) that the API enforces — the three numbers are not independent settings, and updating one may require updating another in the same call.

min ≤ desired ≤ max — the three capacity numbers

Minimum: 2

the group never scales below this

Desired: 4

the active target

Maximum: 10

the group never scales above this

  • Minimum: 2 — the group never scales below this
  • Desired: 4 — the active target
  • Maximum: 10 — the group never scales above this

The three capacity numbers and what each one does

The three capacity numbers and what each one does
SettingMeaning
MinimumThe group never scales below this, regardless of policy
DesiredThe target the group actively tries to maintain
MaximumThe group never scales above this, regardless of policy

Together

bash
aws autoscaling update-auto-scaling-group --auto-scaling-group-name app-asg \
  --min-size 2 --max-size 10 --desired-capacity 4

Remember: min ≤ desired ≤ max bounds and targets group size; a launch template defines what launches; and a lifecycle hook creates a window to run custom logic before an instance is actually launched or terminated.

See also: target tracking and scaling policies · amis and image pipelines

Target tracking, step, and scheduled scaling

coreintermediate

Target tracking is the "just keep this metric near a value" policy — set a target (like 50% CPU) and Auto Scaling adjusts capacity to hold it there, the way a thermostat holds a temperature. Step and scheduled scaling are for cases target tracking doesn't fit: precise multi-tier responses, or capacity known in advance.

Think of it as

Target tracking is a thermostat — set 70°F and it adjusts continuously to hold it. Step scaling is a set of instructions like "if it gets really hot, turn on two fans; if it's only a little hot, turn on one." Scheduled scaling is setting the thermostat higher every day at 5pm because you already know the house gets busy then.

bash
aws autoscaling put-scaling-policy --policy-name cpu-target --policy-type TargetTrackingScaling \
  --target-tracking-configuration file://target-tracking.json

What we're doing: See a scheduled scaling action that raises the minimum ahead of a known daily traffic pattern, layered on top of target tracking.

scheduled-scale-up.shbash
aws autoscaling put-scheduled-update-group-action --auto-scaling-group-name app-asg \
  --scheduled-action-name morning-ramp-up --recurrence "0 8 * * *" \
  --min-size 6 --desired-capacity 6
1
This scheduled action does not replace target tracking — it just raises the floor at a known time, ahead of demand actually arriving.
3
The recurrence "0 8 * * *" runs this every day at 08:00 — capacity is already higher before the morning traffic ramp shows up in any metric.

Why this works: Target tracking alone reacts to a metric after it moves — a scheduled floor set ahead of a known pattern means capacity is already in place before the metric even starts climbing, avoiding the reaction lag that a purely reactive policy has.

Relying on target tracking alone for a workload with a sharp, predictable traffic spike

Wrong

text
# Target tracking only, CPU target 50% — traffic spikes 5x in the first
# minute of a scheduled event

Better

text
# Scheduled scaling raises the floor just before the known event,
# so capacity is already there when the spike hits

What you see: The group scales up correctly, but only after the metric has already breached — leaving a window of degraded performance right when the spike started, before new capacity finished launching.

Why: Target tracking is inherently reactive — it needs the metric to move before it acts, and launching new instances takes real time. A known, predictable spike is exactly the case scheduled (or predictive) scaling is built to get ahead of, rather than reacting after the fact.

A predictable morning spike: reactive vs scheduled-ahead

Target tracking alone

  • +Reacts only after the metric moves
  • +New capacity takes real time to launch
  • +A window of degraded performance right when the spike starts

+ Scheduled scaling

  • Raises the floor ahead of the known time
  • Capacity is already in place before demand arrives
  • No reaction lag
  • Target tracking alone
    • Reacts only after the metric moves
    • New capacity takes real time to launch
    • A window of degraded performance right when the spike starts
  • + Scheduled scaling
    • Raises the floor ahead of the known time
    • Capacity is already in place before demand arrives
    • No reaction lag

Scaling policy types and when each fits

Scaling policy types and when each fits
PolicyFits when
Target trackingA single metric (CPU, request count) should stay near a value — the default choice
Step scalingDifferent alarm breach severities need different, specific capacity responses
Scheduled scalingCapacity needs are known in advance — a daily traffic pattern, a planned event
Predictive scalingLoad has a recurring, learnable historical pattern worth forecasting ahead of

Together

json
{ "TargetValue": 50.0, "PredefinedMetricSpecification": { "PredefinedMetricType": "ASGAverageCPUUtilization" } }

Remember: Target tracking reacts continuously to a metric; step scaling maps specific alarm severities to specific responses; scheduled scaling gets ahead of a known pattern — they combine rather than compete.

See also: asg vocabulary · statelessness behind lb

Auto Scaling and load balancer integration

standardintermediate

An Auto Scaling Group attached to a load balancer's target group automatically registers every new instance and deregisters every instance it terminates — the load balancer's own health check becomes an input to whether Auto Scaling considers an instance healthy.

Think of it as

A staffing agency (Auto Scaling) that automatically adds a new hire to the shift roster (target group) the moment they start, and removes them the moment they leave — the front desk (load balancer) never has to be told separately.

bash
aws autoscaling attach-load-balancer-target-groups --auto-scaling-group-name app-asg \
  --target-group-arns arn:aws:elasticloadbalancing:...:targetgroup/app-tg/...

What we're doing: See the full loop: a target fails its ELB health check, and the ASG treats that as a replacement signal.

health-check-loop.txttext
1. ASG health check type set to ELB (not just EC2)
2. Target group's health check fails for instance i-0abc123 (app returns 500s)
3. ASG marks i-0abc123 unhealthy, based on that ELB signal
4. ASG terminates i-0abc123 and launches a replacement
5. New instance registers with the target group once IT passes health checks
2
This is an application-level failure (bad responses), which EC2's own status checks alone would never catch — the instance itself is running fine.
5
The replacement only starts receiving traffic once it independently passes the same health check the failed instance failed.

Why this works: Using the ELB health check (not just EC2 status checks) as the ASG's health check source is what lets Auto Scaling react to application-level failures, not just infrastructure-level ones — an instance can be "healthy" by EC2's definition while still failing every real request.

Leaving the ASG health check type as EC2 only, for a workload with real application-level failure modes

Wrong

text
# ASG health check type: EC2 (default) — instance passes EC2 status
# checks even though the app itself is returning 500s

Better

text
# ASG health check type: ELB — ties instance health to the actual
# target group health check the load balancer is already running

What you see: An instance serving nothing but error responses stays in the group indefinitely, because EC2's own status checks have no way to know the application layer is broken.

Why: EC2 status checks only verify the underlying hardware and OS are responsive — they say nothing about whether the application running on top is actually working. The ELB health check type is what connects application-level health to the ASG's replacement decisions.

Remember: Attaching a target group automates registration/deregistration — but only setting the ASG's health check type to ELB (not just EC2) lets Auto Scaling react to application-level failures, not just infrastructure ones.

See also: asg vocabulary · listeners rules and target groups

Designing for instance replacement, not permanence

coreintermediate

An Auto Scaling Group can terminate and replace any instance at any time — for health, for a scale-in, for an instance refresh. An application has to be built assuming that can happen, not as an edge case to handle later.

Think of it as

Designing a relay race assuming any runner might need to be swapped mid-lap without dropping the baton — versus assuming the same four runners will finish the whole race, which breaks the moment one of them can't continue.

text
Assume: any instance can be replaced at any time
Design for: externalized state, graceful shutdown window, idempotent work

What we're doing: See what a graceful shutdown handler actually needs to do inside the deregistration delay window.

graceful_shutdown.pypython
def handle_sigterm(signum, frame):
    stop_accepting_new_requests()   # deregistered from target group already
    wait_for_in_flight_requests(timeout=25)   # inside the deregistration delay window
    exit(0)

signal.signal(signal.SIGTERM, handle_sigterm)
2
By the time SIGTERM arrives, the instance is typically already deregistered from the target group — new requests should not be arriving.
5
The timeout here needs to fit inside the load balancer's configured deregistration delay, or in-flight requests get cut off anyway.

Why this works: The deregistration delay is a finite, configured window — an application that assumes it has unlimited time to shut down gracefully will eventually be forcibly terminated mid-work once that window elapses.

Assuming a specific instance will always be available for a scheduled or long-running job

Wrong

text
# A nightly batch job hardcoded to run on instance i-0abc123 specifically

Better

text
# The job runs on whichever instance is currently in the group — or,
# better, on infrastructure designed for exactly this (a scheduled task, not a pet instance)

What you see: The batch job silently stops running after that specific instance is replaced by Auto Scaling for an unrelated reason (a scale-in, a health check failure).

Why: A specific instance ID is not a stable, long-term reference — Auto Scaling makes no promise that any particular instance persists, and depending on one defeats the purpose of using an Auto Scaling Group at all.

What a graceful shutdown does inside the deregistration delay window
health failure,scale-in, or refreshstop acceptingnew requeststimeout ordrained

Running

start

SIGTERM received

Deregistered from target group

Draining in-flight requests

Terminated

end

  • Running (start)
    • → SIGTERM received when health failure, scale-in, or refresh
  • SIGTERM received
    • → Deregistered from target group
  • Deregistered from target group
    • → Draining in-flight requests when stop accepting new requests
  • Draining in-flight requests
    • → Terminated when timeout or drained
  • Terminated (end)

Remember: Any instance in an Auto Scaling Group can be replaced at any time — externalize anything that cannot be lost, handle SIGTERM within the deregistration delay window, and never depend on a specific instance ID persisting.

See also: asg vocabulary · statelessness behind lb · instance replacement over hand tuning

Advertisement