ЁЯПл The SchoolтА║ЁЯУИ ScalingтА║ЁЯУИ рдзрдбрд╛ 06 тАФ Auto Scaling: рд░рд╛рдВрдЧ рд╡рд╛рдврд▓реА рдХреА рдЬрд╛рд╕реНрдд рдЦрд┐рдбрдХреНрдпрд╛
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯУИ рдзрдбрд╛ 06 тАФ Auto Scaling: рд░рд╛рдВрдЧ рд╡рд╛рдврд▓реА рдХреА рдЬрд╛рд╕реНрдд рдЦрд┐рдбрдХреНрдпрд╛

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 13 рдкреИрдХреА рдзрдбрд╛ 06 ┬╖ рдорд╛рдЧреЗ: lesson-05-stateless-load-balancer ┬╖ рдкреБрдвреЗ: lesson-07-kubernetes-scaling


ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ

рдзрдбреЗ 01тАУ05, рдЖрдгрд┐ рддреНрдпрд╛рд╢рд┐рд╡рд╛рдп рдЖрдкреЛрдЖрдк рдЬреЛрдбрд▓реЗ рдЖрдгрд┐ рдХрд╛рдврд▓реЗ рдЬрд╛рдгрд╛рд░реЗ servers: target tracking (servers рд╕реБрдорд╛рд░реЗ 60% рд╡реНрдпрд╕реНрдд рдареЗрд╡рд╛), рдкреНрд░рддреНрдпреЗрдХ рдирд╡реНрдпрд╛ server рд▓рд╛ рдЙрд╢реАрд░ рдХрд░рдгрд╛рд░рд╛ warm-up, рдорд╛рд╣реАрдд рдЕрд╕рд▓реЗрд▓реНрдпрд╛ рдЧрд░реНрджреАрдЖрдзреА scheduled scaling, рдЖрдгрд┐ рд╣рд│реВрд╣рд│реВ scale in. scale/demo.py рдордзрд▓реЗ autoscale() рдирд┐рдХрд╛рд▓рд╛рдЪреНрдпрд╛ рджрд┐рд╡рд╕рд╛рдЪреА 12 рдорд┐рдирд┐рдЯреЗ рдЪрд╛рд▓рд╡реВрди рджрд╛рдЦрд╡рддреЗ.

ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ

рджреАрдкрд┐рдХрд╛рдЪрд╛ рдПрдХ рдирд┐рдпрдо рдЖрд╣реЗ: "рдкреНрд░рддреНрдпреЗрдХ рдХрд╛рд░рдХреВрди рджрд░ 10 рдорд┐рдирд┐рдЯрд╛рдВрдкреИрдХреА рд╕реБрдорд╛рд░реЗ 6 рдорд┐рдирд┐рдЯреЗ рд╡реНрдпрд╕реНрдд рдЕрд╕рд╛рд╡реА". рддреНрдпрд╛ рдЬрд╛рд╕реНрдд рд╡реНрдпрд╕реНрдд рдЕрд╕рддреАрд▓ рддрд░ рддреА staff room рдордзреВрди рдЖрдгрдЦреА рдХрд╛рд░рдХреВрди рдмреЛрд▓рд╛рд╡рддреЗ.

рдкрдг рдирд╡реНрдпрд╛ рдХрд╛рд░рдХреБрдирд╛рд▓рд╛ рдЪрд╛рд▓рдд рдпреЗрдКрди, рдЯреЗрдмрд▓ рд▓рд╛рд╡реВрди рдЖрдгрд┐ рдкреИрд╢рд╛рдВрдЪреА рдкреЗрдЯреА рдЙрдШрдбрд╛рдпрд▓рд╛ 2 рдорд┐рдирд┐рдЯреЗ рд▓рд╛рдЧрддрд╛рдд. рдирд┐рдХрд╛рд▓ рд▓рд╛рдЧрддреЛ. рдПрдХрд╛ рдорд┐рдирд┐рдЯрд╛рдд рдЧрд░реНрджреА 150 рдкрд╛рд▓рдХрд╛рдВрд╡рд░реВрди 900 рд╣реЛрддреЗ, рдордЧ 1,800. рджреАрдкрд┐рдХрд╛ рд▓рдЧреЗрдЪ рдХрд╛рд░рдХреВрди рдмреЛрд▓рд╛рд╡рддреЗ тАФ рдкрдг рджреЛрди рдорд┐рдирд┐рдЯреЗ рдлрдХреНрдд рдЬреБрдиреНрдпрд╛ 2 рдЦрд┐рдбрдХреНрдпрд╛рдЪ рдЙрдШрдбреНрдпрд╛ рдЕрд╕рддрд╛рдд, рдЖрдгрд┐ рд░рд╛рдВрдЧ рдлрд╛рдЯрдХрд╛рдмрд╛рд╣реЗрд░ рдЬрд╛рддреЗ.

рдкреБрдврдЪреНрдпрд╛ рд╡рд░реНрд╖реА рджреАрдкрд┐рдХрд╛ рдЬрд╛рд╕реНрдд рд╣реБрд╢рд╛рд░ рдЕрд╕рддреЗ. рдирд┐рдХрд╛рд▓ рдХрдзреА рд▓рд╛рдЧрдгрд╛рд░ рд╣реЗ рддрд┐рд▓рд╛ рдорд╛рд╣реАрдд рдЕрд╕рддреЗ. рдореНрд╣рдгреВрди рддреА 20 рдХрд╛рд░рдХреБрдирд╛рдВрдирд╛ рддреНрдпрд╛ рд╡реЗрд│реЗрдЪреНрдпрд╛ рдЖрдзреАрдЪ рдЖрдкрд╛рдкрд▓реНрдпрд╛ рдЯреЗрдмрд▓рд╛рдВрд╡рд░ рдпрд╛рдпрд▓рд╛ рд╕рд╛рдВрдЧрддреЗ. рд░рд╛рдВрдЧрдЪ рдирд╛рд╣реА.

рдЧрд░реНрджреА рдШрд░реА рдЧреЗрд▓реНрдпрд╛рд╡рд░ рддреА рдХрд╛рд░рдХреБрдирд╛рдВрдирд╛ рдПрдХреЗрдХ рдХрд░реВрди рдкрд░рдд рдкрд╛рдард╡рддреЗ, рди рдЬрд╛рдгреЛ рджреБрд╕рд░реА рд▓рд╛рдЯ рдЖрд▓реА рддрд░.

ЁЯЧ║я╕П рдЖрдХреГрддреА

flowchart LR
    cw["ЁЯУК CloudWatch metric<br/>CPU or requests per target"] --> tt["ЁЯОп target tracking<br/>keep it near 60%"]
    tt -->|"too busy: add"| asg["ЁЯУИ Auto Scaling group<br/>min 2 ┬╖ max 20"]
    tt -->|"too idle: remove one"| asg
    sch["ЁЯЧУя╕П scheduled action<br/>min 20 before results"] --> asg
    asg --> wu["тП│ warm-up 2 min<br/>then it serves"]
    wu --> alb["тЪЦя╕П load balancer targets"]

ЁЯЧ║я╕П рдХрд╛рдврд▓реЗрд▓реА рдЖрдХреГрддреА + рдПрдХ lab: https://school-edh.pages.dev/scaling/lesson-diagrams.html#l06

тЭУ рдХрд╛рдп

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг рдПрдХ рд╕рдХрд╛рд│ рдЯрд┐рдХрдгреНрдпрд╛рд╕рд╛рдареА рд╡рд░реНрд╖рднрд░ 20 servers рдЪреЗ рдкреИрд╕реЗ рднрд░рдгреЗ рдореНрд╣рдгрдЬреЗ рдЙрдзрд│рдкрдЯреНрдЯреА, рдЖрдгрд┐ рддреНрдпрд╛ рд╕рдХрд╛рд│реА 2 servers рдореНрд╣рдгрдЬреЗ outage. Auto Scaling load рдЪреНрдпрд╛ рдорд╛рдЧреЛрдорд╛рдЧ рдЬрд╛рддреЗ. рдкрдг рддреЗ рдкреНрд░рддрд┐рдХреНрд░рд┐рдпрд╛ рджреЗрддреЗ: рддреЗ рдЧрд░реНрджреА рдкрд╛рд╣рддреЗ, рдордЧ servers рдорд╛рдЧрддреЗ, рдордЧ рддреЗ warm up рд╣реЛрдИрдкрд░реНрдпрдВрдд рдерд╛рдВрдмрддреЗ. рдпреЗрддрд╛рдирд╛ рджрд┐рд╕рдгрд╛рд▒реНрдпрд╛ рдЧрд░реНрджреАрд╕рд╛рдареА тАФ 9:00 рд▓рд╛ рдирд┐рдХрд╛рд▓ тАФ scheduled action рдХреЛрдгрддреНрдпрд╛рд╣реА рдкреНрд░рддрд┐рдХреНрд░рд┐рдпреЗрдкреЗрдХреНрд╖рд╛ рд╕рд░рд╕ рдард░рддреЗ.

ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)

scale/sim.py рдордзрд▓реЗ autoscale(load, per_server, target=0.6, start=2, lo=2, hi=20, warmup=2) рджрд░ рдорд┐рдирд┐рдЯрд╛рд▓рд╛ рдПрдХ рдкрд╛рдКрд▓ рдЯрд╛рдХрддреЗ. рддреНрдпрд╛рд▓рд╛ рд╣рд╡реЗ рдЕрд╕рд▓реЗрд▓реЗ servers рддреЗ рдореЛрдЬрддреЗ (ceil(rps / (per_server ├Ч 0.6)), lo рдЖрдгрд┐ hi рдЪреНрдпрд╛ рдордзреНрдпреЗ рдареЗрд╡реВрди), рдирд╡реЗ servers рдорд╛рдЧрд╡рддреЗ рдЬреЗ warmup рдорд┐рдирд┐рдЯрд╛рдВрдирдВрддрд░ рд╕реЗрд╡рд╛ рджреНрдпрд╛рдпрд▓рд╛ рд▓рд╛рдЧрддрд╛рдд, рдЖрдгрд┐ рдХрдореА рд╣рд╡реЗ рдЕрд╕рддреАрд▓ рддреЗрд╡реНрд╣рд╛ рдорд┐рдирд┐рдЯрд╛рд▓рд╛ рдПрдХ server рдХрд╛рдврддреЗ. рдкреНрд░рддреНрдпреЗрдХ рдУрд│ рдореНрд╣рдгрдЬреЗ (рдорд┐рдирд┐рдЯ, req/s, рд╕реЗрд╡рд╛ рджреЗрдгрд╛рд░реЗ, рд╡реНрдпрд╕реНрдд %, рдХреНрд╖рдорддреЗрдкреЗрдХреНрд╖рд╛ рдЬрд╛рд╕реНрдд requests тАФ рдкреНрд░рддреНрдпрдХреНрд╖рд╛рдд рд╣рд│реВ рдЙрддреНрддрд░реЗ рдХрд┐рдВрд╡рд╛ 5xx errors). Model рдордзреНрдпреЗ policy рд▓рд╛ рдЦрд░реА рдорд╛рдЧрдгреА рджрд┐рд╕рддреЗ, request-count metric рд╕рд╛рд░рдЦреА; CPU metric 100% рд╡рд░ рдерд╛рдВрдмрддреЛ рдЖрдгрд┐ рдЧрд░реНрджреА рдХрд┐рддреА рдореЛрдареА рдЖрд╣реЗ рддреЗ рд▓рдкрд╡рддреЛ.

ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛

python3 scale/demo.py autoscale
python3 - <<'EOF'
import sys; sys.path.insert(0, "scale"); from sim import autoscale
load = [150, 150, 900, 1800, 2400, 2400, 2400, 1200, 600, 300, 300, 300]
for label, kw in (("as in the demo", {}), ("warm-up 1 minute", dict(warmup=1)), ("warm-up 4 minutes", dict(warmup=4)),
                  ("max size 30", dict(hi=30)), ("scheduled: min 20 all day", dict(start=20, lo=20))):
    rows = autoscale(load, 150, **kw)
    print(f"{label:<26} over capacity (sum) {sum(r[4] for r in rows):>5} ┬╖ most servers {max(r[2] for r in rows)} ┬╖ util at minute 6: {rows[6][3]}%")
EOF
python3 scale/test_scale.py

тЬЕ рддрдкрд╛рд╕рд╛ тАФ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рджрд┐рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ

autoscale рд╣реЗ print рдХрд░рддреЗ (рдкрд╣рд┐рд▓реНрдпрд╛ рдУрд│реА):

   min  req/s  serving  util  over capacity (slow / 5xx)
     0    150        2   50%        0
     1    150        2   50%        0
     2    900        2  100%      600
     3   1800        2  100%     1500
     4   2400       10  100%      900
     5   2400       20   80%        0

рдордЧ 7 1200 20 40% 0, 8 600 19 21% 0 тАж рдЦрд╛рд▓реА 11 300 16 12% 0 рдкрд░реНрдпрдВрдд тАФ рджрд░ рдорд┐рдирд┐рдЯрд╛рд▓рд╛ рдПрдХ server рдХрдореА.

рддреБрдордЪрд╛ snippet рд╣реЗ print рдХрд░рддреЛ:

as in the demo             over capacity (sum)  3000 ┬╖ most servers 20 ┬╖ util at minute 6: 80%
warm-up 1 minute           over capacity (sum)   900 ┬╖ most servers 20 ┬╖ util at minute 6: 80%
warm-up 4 minutes          over capacity (sum)  7200 ┬╖ most servers 20 ┬╖ util at minute 6: 100%
max size 30                over capacity (sum)  3000 ┬╖ most servers 27 ┬╖ util at minute 6: 59%
scheduled: min 20 all day  over capacity (sum)     0 ┬╖ most servers 20 ┬╖ util at minute 6: 80%

Tests рдордзреНрдпреЗ тЬЕ L06 new servers arrive only after the warm-up рдЖрдгрд┐ тЬЕ L06 scale in one server at a time рдЕрд╕рддрд╛рдд.

ЁЯПБ рддреБрдореНрд╣реА рдЖрддреНрддрд╛рдЪ рдХрд╛рдп рд╕рд┐рджреНрдз рдХреЗрд▓реЗ

рдЧрд░реНрджреАрд▓рд╛ рдХрд┐рддреА рддреНрд░рд╛рд╕ рд╣реЛрддреЛ рддреЗ warm-up рдард░рд╡рддреЛ: 1 рдорд┐рдирд┐рдЯ тЖТ 900, 2 рдорд┐рдирд┐рдЯреЗ тЖТ 3,000, 4 рдорд┐рдирд┐рдЯреЗ тЖТ 7,200. рдореЛрдард╛ maximum (30) рдЧрдЯрд╛рд▓рд╛ 60% target рдЧрд╛рдареВ рджреЗрддреЛ (27 servers, 59%) рдкрдг рдкрд╣рд┐рд▓реНрдпрд╛ рдорд┐рдирд┐рдЯрд╛рдВрдирд╛ рдорджрдд рдХрд░рдд рдирд╛рд╣реА тАФ рдлрдХреНрдд рдЖрдзреА рдЖрд▓реЗрд▓реЗ servers рдХрд░рддрд╛рдд. Scheduled minimum overflow рдкреВрд░реНрдгрдкрдгреЗ рдХрд╛рдвреВрди рдЯрд╛рдХрддреЛ. рдЖрдгрд┐ 20 servers рд╡рд░ рдЧрдЯ 80% рд╡рд░ рд░рд╛рд╣рддреЛ: рддреНрдпрд╛рд▓рд╛ 27 рд╣рд╡реЗ рдЕрд╕рддрд╛рдирд╛ рддреНрдпрд╛рдЪреНрдпрд╛ maximum рдиреЗ рддреНрдпрд╛рд▓рд╛ 20 рд╡рд░ рдерд╛рдВрдмрд╡рд▓реЗ.

тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд

On a real account тАФ requests per target рд╡рд░ target tracking (рд╣рд╛ metric рдкреНрд░рддреНрдпреЗрдХ target рдЪреНрдпрд╛ рдорд┐рдирд┐рдЯрд╛рдЪреНрдпрд╛ requests рдореЛрдЬрддреЛ: 90 req/s ├Ч 60 = 5,400), 2 рдорд┐рдирд┐рдЯрд╛рдВрдЪрд╛ default warm-up, рдЖрдгрд┐ рдирд┐рдХрд╛рд▓рд╛рдЪреНрдпрд╛ рджрд┐рд╡рд╕рд╛рдЖрдзреА scheduled minimum:

aws autoscaling update-auto-scaling-group --auto-scaling-group-name school-api \
    --min-size 2 --max-size 30 --default-instance-warmup 120

aws autoscaling put-scaling-policy --auto-scaling-group-name school-api \
    --policy-name requests-per-target --policy-type TargetTrackingScaling \
    --target-tracking-configuration '{
      "TargetValue": 5400,
      "PredefinedMetricSpecification": {
        "PredefinedMetricType": "ALBRequestCountPerTarget",
        "ResourceLabel": "app/school-alb/50dc6c495c0c9188/targetgroup/school-api/6d0ecf831eec9f09" } }'

aws autoscaling put-scheduled-update-group-action --auto-scaling-group-name school-api \
    --scheduled-action-name results-day --start-time 2026-05-20T03:00:00Z \
    --min-size 20 --desired-capacity 20
aws autoscaling put-scheduled-update-group-action --auto-scaling-group-name school-api \
    --scheduled-action-name results-day-over --start-time 2026-05-20T09:00:00Z --min-size 2

рдореЛрдареЗ рдХрд╛рдо рд╕рдВрдкрд╡рдд рдЕрд╕рд▓реЗрд▓реНрдпрд╛ рдПрдХрд╛ instance рдЪреЗ рд╕рдВрд░рдХреНрд╖рдг рдХрд░рд╛:

aws autoscaling set-instance-protection --auto-scaling-group-name school-api \
    --instance-ids i-0123456789abcdef0 --protected-from-scale-in

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: event рдирдВрддрд░ ASG рдЪрд╛ activity history load balancer рдЪреНрдпрд╛ 5xx count рдЖрдгрд┐ p99 рд╢реА рддреБрд▓рдирд╛ рдХрд░рд╛. рдирд╡реЗ instances рд╕реЗрд╡реЗрдд рдпреЗрдгреНрдпрд╛рдЖрдзреА errors рдЖрд▓реЗ рдЕрд╕рддреАрд▓, рддрд░ schedule рдЖрдзреА рд╕реБрд░реВ рдХрд░рд╛ рдХрд┐рдВрд╡рд╛ minimum рд╡рд╛рдврд╡рд╛.

тПня╕П рдкреБрдвреЗ

рдкреВрд░реНрдг servers рдореНрд╣рдгрдЬреЗ рдореЛрдареА рдкрд╛рд╡рд▓реЗ, рдЖрдгрд┐ рддреНрдпрд╛рдВрдирд╛ рдорд┐рдирд┐рдЯреЗ рд▓рд╛рдЧрддрд╛рдд. Kubernetes рд╡рд░ рдЕрдиреЗрдХ рд▓рд╣рд╛рди pods nodes рд╡рд╛рдЯреВрди рдШреЗрддрд╛рдд тАФ рдЖрдгрд┐ рджреЛрди autoscalers рдЕрд╕рддрд╛рдд, рдкреНрд░рддреНрдпреЗрдХрд╛рд╕рд╛рдареА рдПрдХ.

git checkout lesson-07-kubernetes-scaling

ЁЯУИ Lesson 06 тАФ Auto Scaling: more counters when the queue grows

ЁЯУН You are here: Lesson 06 of 13 ┬╖ Previous: lesson-05-stateless-load-balancer ┬╖ Next: lesson-07-kubernetes-scaling


ЁЯУж What's in this branch

Lessons 01тАУ05, plus servers that are added and removed by themselves: target tracking (keep the servers about 60% busy), the warm-up that delays every new server, scheduled scaling before a known spike, and scaling in slowly. autoscale() in scale/demo.py plays 12 minutes of results day.

ЁЯзТ Explain like I'm 5

Dipika has a rule: "each clerk should be busy about 6 minutes in every 10". If they are busier, she calls more clerks from the staff room.

But a new clerk needs 2 minutes to walk over, set up the desk and open the cash box. The results go up. In one minute the crowd grows from 150 parents to 900, then 1,800. Dipika calls clerks at once тАФ but for two minutes, only the old 2 counters are open, and the queue goes out of the gate.

Next year Dipika is smarter. She knows when the results go up. So she asks 20 clerks to be at their desks before that time. No queue at all.

When the crowd goes home, she sends clerks back one at a time, in case a second wave comes.

ЁЯЧ║я╕П Diagram

flowchart LR
    cw["ЁЯУК CloudWatch metric<br/>CPU or requests per target"] --> tt["ЁЯОп target tracking<br/>keep it near 60%"]
    tt -->|"too busy: add"| asg["ЁЯУИ Auto Scaling group<br/>min 2 ┬╖ max 20"]
    tt -->|"too idle: remove one"| asg
    sch["ЁЯЧУя╕П scheduled action<br/>min 20 before results"] --> asg
    asg --> wu["тП│ warm-up 2 min<br/>then it serves"]
    wu --> alb["тЪЦя╕П load balancer targets"]

ЁЯЧ║я╕П Drawn version + a lab: https://school-edh.pages.dev/scaling/lesson-diagrams.html#l06

тЭУ What

ЁЯдФ Why

Because paying for 20 servers all year to survive one morning is waste, and 2 servers on that morning is an outage. Auto Scaling follows the load. But it reacts: it sees the crowd, then asks for servers, then waits for them to warm up. For a spike you can see coming тАФ results at 9:00 тАФ a scheduled action beats any reaction.

ЁЯФз How (in this repo)

autoscale(load, per_server, target=0.6, start=2, lo=2, hi=20, warmup=2) in scale/sim.py takes one step per minute. It computes the servers it wants (ceil(rps / (per_server ├Ч 0.6)), kept between lo and hi), orders new ones that start serving warmup minutes later, and removes one server a minute when it wants fewer. Each row is (minute, req/s, serving, busy %, requests over capacity тАФ slow answers or 5xx errors in real life). In the model the policy sees the real demand, like a request-count metric; a CPU metric stops at 100% and hides how big the crowd is.

ЁЯзк Try it

python3 scale/demo.py autoscale
python3 - <<'EOF'
import sys; sys.path.insert(0, "scale"); from sim import autoscale
load = [150, 150, 900, 1800, 2400, 2400, 2400, 1200, 600, 300, 300, 300]
for label, kw in (("as in the demo", {}), ("warm-up 1 minute", dict(warmup=1)), ("warm-up 4 minutes", dict(warmup=4)),
                  ("max size 30", dict(hi=30)), ("scheduled: min 20 all day", dict(start=20, lo=20))):
    rows = autoscale(load, 150, **kw)
    print(f"{label:<26} over capacity (sum) {sum(r[4] for r in rows):>5} ┬╖ most servers {max(r[2] for r in rows)} ┬╖ util at minute 6: {rows[6][3]}%")
EOF
python3 scale/test_scale.py

тЬЕ Verify тАФ what you should see

autoscale prints (first rows):

   min  req/s  serving  util  over capacity (slow / 5xx)
     0    150        2   50%        0
     1    150        2   50%        0
     2    900        2  100%      600
     3   1800        2  100%     1500
     4   2400       10  100%      900
     5   2400       20   80%        0

then 7 1200 20 40% 0, 8 600 19 21% 0 тАж down to 11 300 16 12% 0 тАФ one server fewer each minute.

Your snippet prints:

as in the demo             over capacity (sum)  3000 ┬╖ most servers 20 ┬╖ util at minute 6: 80%
warm-up 1 minute           over capacity (sum)   900 ┬╖ most servers 20 ┬╖ util at minute 6: 80%
warm-up 4 minutes          over capacity (sum)  7200 ┬╖ most servers 20 ┬╖ util at minute 6: 100%
max size 30                over capacity (sum)  3000 ┬╖ most servers 27 ┬╖ util at minute 6: 59%
scheduled: min 20 all day  over capacity (sum)     0 ┬╖ most servers 20 ┬╖ util at minute 6: 80%

The tests include тЬЕ L06 new servers arrive only after the warm-up and тЬЕ L06 scale in one server at a time.

ЁЯПБ What you just proved

The warm-up decides how much the crowd suffers: 1 minute тЖТ 900, 2 minutes тЖТ 3,000, 4 minutes тЖТ 7,200. A bigger maximum (30) lets the group reach the 60% target (27 servers, 59%) but does not help the first minutes тАФ only earlier servers do. The scheduled minimum removes the overflow completely. And at 20 servers the group sits at 80%: its maximum stopped it at 20 when it wanted 27.

тЪая╕П Common mistakes

ЁЯПн In production

On a real account тАФ target tracking on requests per target (the metric counts requests per target per minute: 90 req/s ├Ч 60 = 5,400), a 2-minute default warm-up, and a scheduled minimum before results day:

aws autoscaling update-auto-scaling-group --auto-scaling-group-name school-api \
    --min-size 2 --max-size 30 --default-instance-warmup 120

aws autoscaling put-scaling-policy --auto-scaling-group-name school-api \
    --policy-name requests-per-target --policy-type TargetTrackingScaling \
    --target-tracking-configuration '{
      "TargetValue": 5400,
      "PredefinedMetricSpecification": {
        "PredefinedMetricType": "ALBRequestCountPerTarget",
        "ResourceLabel": "app/school-alb/50dc6c495c0c9188/targetgroup/school-api/6d0ecf831eec9f09" } }'

aws autoscaling put-scheduled-update-group-action --auto-scaling-group-name school-api \
    --scheduled-action-name results-day --start-time 2026-05-20T03:00:00Z \
    --min-size 20 --desired-capacity 20
aws autoscaling put-scheduled-update-group-action --auto-scaling-group-name school-api \
    --scheduled-action-name results-day-over --start-time 2026-05-20T09:00:00Z --min-size 2

Protect one instance that is finishing a long job:

aws autoscaling set-instance-protection --auto-scaling-group-name school-api \
    --instance-ids i-0123456789abcdef0 --protected-from-scale-in

ЁЯПн Why this matters in production: after the event, compare the ASG's activity history with the load balancer's 5xx count and p99. If errors came before the new instances were in service, start the schedule earlier or raise the minimum.

тПня╕П Next

Whole servers are big steps and take minutes. On Kubernetes, many small pods share nodes тАФ and there are two autoscalers, one for each.

git checkout lesson-07-kubernetes-scaling
тЖР Previousstateless load balancerNext тЖТkubernetes scaling

This page is the lesson's README from the lesson-06-auto-scaling branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.