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

ЁЯУИ рдзрдбрд╛ 07 тАФ Capacity planning: рд╡рд┐рджреНрдпрд╛рд░реНрдерд┐рдиреА рдпреЗрдгреНрдпрд╛рдЖрдзреАрдЪ рдкреБрдврдЪреНрдпрд╛ рд╕рддреНрд░рд╛рд╕рд╛рдареА рдЦреБрд░реНрдЪреНрдпрд╛

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 07 ┬╖ рдорд╛рдЧреЗ: lesson-06-progressive-delivery ┬╖ рдкреБрдвреЗ: lesson-08-incident-command


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

рдзрдбрд╛ 06, рдЕрдзрд┐рдХ рдирд┐рдпреЛрдЬрди. рдЦрд▒реНрдпрд╛ peaks рд╡рд░реВрди рд╡рд╛рдвреАрдЪреА рд░реЗрд╖рд╛ рдХрд╛рдврд╛, рддрд┐рдЪрд╛ forecast рдХрд░рд╛, headroom рдареЗрд╡рд╛, рдмрд┐рдШрд╛рдбрд╛рдВрд╕рд╛рдареА рдЬрд╛рд╕реНрддреАрдЪреЗ servers рдЬреЛрдбрд╛ (N+1, N+2), рд╕рдВрдкреВрд░реНрдг zone рдЧреЗрд▓рд╛ рддрд░реА рдЯрд┐рдХрд╛, рдЖрдгрд┐ рдард╛рдКрдХ рдЕрд╕рд▓реЗрд▓реНрдпрд╛ events (рдирд┐рдХрд╛рд▓рд╛рдЪрд╛ рджрд┐рд╡рд╕) рдЪрд╛ рдЖрд░рд╛рдЦрдбрд╛ рд╡реЗрдЧрд│рд╛ рдХрд░рд╛. sre/crew.py рдордзреАрд▓ CapacityPlan рдЖрдгрд┐ sre/demo.py рдордзреАрд▓ capacity(). Load рдХрд╕рд╛ рдореЛрдЬрд╛рдпрдЪрд╛ рдЖрдгрд┐ autoscale рдХрд╕реЗ рдХрд░рд╛рдпрдЪреЗ рд╣реЗ Scaling school рдордзреНрдпреЗ рд╢рд┐рдХрд╡рд▓реЗ рдЖрд╣реЗ; рд╣рд╛ рдзрдбрд╛ рдореНрд╣рдгрдЬреЗ рдкрдердХрд╛рдЪрд╛ рдЖрд░рд╛рдЦрдбрд╛.

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

рджрд░ рдЖрдард╡рдбреНрдпрд╛рд▓рд╛ рдРрд╢реНрд╡рд░реНрдпрд╛ рд╕рд░реНрд╡рд╛рдд рдЧрд░реНрджреАрдЪреНрдпрд╛ рдХреНрд╖рдгреА рд╕рднрд╛рдЧреГрд╣рд╛рдд рдХрд┐рддреА рд╡рд┐рджреНрдпрд╛рд░реНрдерд┐рдиреА рдЖрд╣реЗрдд рддреЗ рдореЛрдЬрддреЗ. ЁЯкС рдЖрдард╡рдбрд╛ 1: 1,206. рдЖрдард╡рдбрд╛ 12: 1,707. рдЖрдХрдбрд╛ рдЖрдард╡рдбреНрдпрд╛рд▓рд╛ рд╕рд╛рдзрд╛рд░рдг 48 рдиреЗ рд╡рд╛рдврддреЛ.

"рд╕рд╣рд╛ рдорд╣рд┐рдиреНрдпрд╛рдВрдиреА рддреЛ рдХреБрдареЗ рдЕрд╕реЗрд▓?" рддреА рдпрд╛ рдЖрдХрдбреНрдпрд╛рдВрдордзреВрди рдПрдХ рд╕рд░рд│ рд░реЗрд╖рд╛ рдХрд╛рдврддреЗ рдЖрдгрд┐ рддреА рдкреБрдвреЗ рдиреЗрддреЗ: рд╕рд╛рдзрд╛рд░рдг 2,963.

рдЦреБрд░реНрдЪреНрдпрд╛рдВрдЪреНрдпрд╛ рдПрдХрд╛ рд░рд╛рдВрдЧреЗрдд 300 рдЬрдг рдмрд╕рддрд╛рдд, рдкрдг рдкреВрд░реНрдг рднрд░рд▓реЗрд▓реА рд░рд╛рдВрдЧ рдЕрд╡рдШрдбрд▓реЗрд▓реА рдЕрд╕рддреЗ, рдореНрд╣рдгреВрди рддреА рдкреНрд░рддреНрдпреЗрдХ рд░рд╛рдВрдЧреЗрдЪрд╛ рдЖрд░рд╛рдЦрдбрд╛ 210 рд╕рд╛рдареА рдХрд░рддреЗ (70% рднрд░рд▓реЗрд▓реА). рдореНрд╣рдгрдЬреЗ 15 рд░рд╛рдВрдЧрд╛.

"рдЖрдгрд┐ рдПрдЦрд╛рджреА рд░рд╛рдВрдЧ рдореЛрдбрд▓реА рддрд░?" рдПрдХ рдЬреЛрдбрд╛: 16. "рдПрдХрд╛ рд░рд╛рдВрдЧреЗрдЪреА рджреБрд░реБрд╕реНрддреА рдЪрд╛рд▓реВ рдЕрд╕рддрд╛рдирд╛ рдЖрдгрд┐ рджреБрд╕рд░реА рдореЛрдбрд▓реА рддрд░?" рджреЛрди рдЬреЛрдбрд╛: 17. "рд╕рднрд╛рдЧреГрд╣рд╛рдЪреА рд╕рдВрдкреВрд░реНрдг рдкреВрд░реНрд╡ рдмрд╛рдЬреВ рдмрдВрдж рдЕрд╕рд▓реА рддрд░?" рдЙрд░рд▓реЗрд▓реНрдпрд╛ рджреЛрди рдмрд╛рдЬреВрдВрдиреА рд╕рдЧрд│реНрдпрд╛рдВрдирд╛ рдмрд╕рд╡рд╛рдпрд▓рд╛ рд╣рд╡реЗ: 24 рд░рд╛рдВрдЧрд╛, рддреАрди рдмрд╛рдЬреВрдВрдордзреНрдпреЗ рд╡рд┐рднрд╛рдЧрд▓реЗрд▓реНрдпрд╛.

рдЖрдгрд┐ рдирд┐рдХрд╛рд▓рд╛рдЪрд╛ рджрд┐рд╡рд╕ рд╡реЗрдЧрд│рд╛ рдЕрд╕рддреЛ: рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЧрд░реНрджреАрдЪреНрдпрд╛ 2.5 ├Ч. рддреНрдпрд╛рд▓рд╛ 36 рд░рд╛рдВрдЧрд╛ рд▓рд╛рдЧрддрд╛рдд тАФ рдореНрд╣рдгреВрди рддреНрдпрд╛ рддреНрдпрд╛ рджрд┐рд╡рд╕рд╛рдЪреНрдпрд╛ рдЖрдзреАрдЪ рдЦреБрд░реНрдЪреНрдпрд╛ рдЙрд╕рдиреНрдпрд╛ рдЖрдгрддрд╛рдд, рдкрд╛рд▓рдХ рдЙрднреЗ рдЕрд╕рддрд╛рдирд╛ рдирд╛рд╣реА.

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

flowchart LR
    peaks["ЁЯУК 12 weekly peaks<br/>1,206 тЖТ 1,707 req/s"] --> fit["ЁЯУИ straight line<br/>+47.8 req/s per week"]
    fit --> fc["forecast +26 weeks<br/>2,963 req/s"]
    fc --> n["N = 15<br/>210 req/s each (70% of 300)"]
    n --> n1["N+1 = 16"]
    n --> n2["N+2 = 17"]
    n --> z["lose 1 of 3 zones: 24"]
    fc --> ev["results day ├Ч 2.5<br/>7,408 req/s тЖТ 36"]

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

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

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг capacity рд╕рдВрдкрдгреЗ рд╣реЗ рдЕрд╕реЗ outage рдЖрд╣реЗ рдЬреЗ рддреБрдореНрд╣рд╛рд▓рд╛ рдорд╣рд┐рдиреЛрдиреНрдорд╣рд┐рдиреЗ рдЖрдзреА рдпреЗрддрд╛рдирд╛ рджрд┐рд╕реВ рд╢рдХрддреЗ. рдЖрдгрд┐ рдЧрд░рдЬреЗрдкреЗрдХреНрд╖рд╛ рдЦреВрдк рдЬрд╛рд╕реНрдд рд╡рд┐рдХрдд рдШреЗрдгреЗ рдореНрд╣рдгрдЬреЗ рджрд░ рдорд╣рд┐рдиреНрдпрд╛рд▓рд╛ рд╡рд╛рдпрд╛ рдЬрд╛рдгрд╛рд░реЗ рдкреИрд╕реЗ. рдЖрд░рд╛рдЦрдбрд╛ рджреЛрдиреНрд╣реАрд╡рд░ рдЖрдХрдбреЗ рдареЗрд╡рддреЛ, рдореНрд╣рдгреВрди "рдорд╛рд░реНрдЪрдкрд░реНрдпрдВрдд рдЖрдкрд▓реНрдпрд╛рд▓рд╛ рдЖрдгрдЦреА 9 servers рд▓рд╛рдЧрддреАрд▓" рд╣рд╛ рдкреБрд░рд╛рд╡реНрдпрд╛рд╕рд╣ рдХреЗрд▓реЗрд▓рд╛ рдпреБрдХреНрддрд┐рд╡рд╛рдж рдард░рддреЛ. рддреЛ рд▓рдкрд▓реЗрд▓реА рдЧрд░рдЬрд╣реА рдЙрдШрдб рдХрд░рддреЛ: рдмрд┐рдШрд╛рдбрд╛рдЪрд╛ рдЖрд░рд╛рдЦрдбрд╛ (рдПрдХ zone рдмрдВрдж) рдмрд╣реБрддреЗрдХ рд╡реЗрд│рд╛ рд╡рд╛рдвреАрдЪреНрдпрд╛ рдЖрд░рд╛рдЦрдбреНрдпрд╛рдкреЗрдХреНрд╖рд╛ рдЬрд╛рд╕реНрдд рдорд╛рдЧрддреЛ.

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

sre/crew.py рдордзреНрдпреЗ CapacityPlan(per_server_rps=300, target_util=0.7, zones=3). weekly_peaks() 12 seeded рд╕рд╛рдкреНрддрд╛рд╣рд┐рдХ peaks рдмрдирд╡рддреЗ (рд╡рд╛рдв рдЕрдзрд┐рдХ noise), fit(ys) least-squares slope рдЖрдгрд┐ intercept рдкрд░рдд рджреЗрддреЗ, servers(peak) рдореНрд╣рдгрдЬреЗ тМИpeak ├╖ (300 ├Ч 0.7)тМЙ рдЖрдгрд┐ zone_safe(n) рдореНрд╣рдгрдЬреЗ 3 ├Ч тМИn ├╖ 2тМЙ. capacity() рдЖрдард╡рдбрд╛ 12 рдирдВрддрд░ 26 рдЖрдард╡рдбреЗ рдкреБрдврдЪрд╛ forecast рдХрд░рддреЗ.

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

python3 sre/demo.py capacity
python3 - <<'EOF'
import sys; sys.path.insert(0, "sre"); from crew import CapacityPlan
plan = CapacityPlan()
peaks = plan.weekly_peaks(12); slope, icpt = plan.fit(peaks)
for ahead in (4, 13, 26, 52):
    fc = round(icpt + slope * (11 + ahead)); n = plan.servers(fc)
    print(f"{ahead:>2} weeks ahead: {fc:>5,} req/s тЖТ N {n:>2} ┬╖ N+2 {n + 2:>2} ┬╖ zone-safe {plan.zone_safe(n):>2}")
for util in (0.5, 0.7, 0.9):
    print(f"target utilisation {util:.0%} тЖТ N = {CapacityPlan(target_util=util).servers(2963)} for 2,963 req/s")
EOF

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

capacity рд╣реЗ рдЫрд╛рдкрддреЗ:

тФАтФА weekly peak requests/s, 12 weeks: [1206, 1226, 1276, 1373, 1365, 1448, 1474, 1533, 1600, 1613, 1684, 1707]
   straight-line fit: +47.8 req/s per week ┬╖ forecast 26 weeks ahead: 2,963 req/s
   one server: 300 req/s at its load-test limit; plan at 70% = 210 тЖТ N = 15 servers for the forecast peak
   N+1 = 16 (one server can fail) ┬╖ N+2 = 17 (one in maintenance AND one fails) ┬╖ survive losing 1 of 3 zones: 24
   results day (2.5 ├Ч the normal peak = 7,408 req/s) needs 36 тАФ plan it as an event, pre-scale, don't wait for autoscaling

рддреБрдордЪрд╛ snippet рд╣реЗ рдЫрд╛рдкрддреЛ:

 4 weeks ahead: 1,912 req/s тЖТ N 10 ┬╖ N+2 12 ┬╖ zone-safe 15
13 weeks ahead: 2,342 req/s тЖТ N 12 ┬╖ N+2 14 ┬╖ zone-safe 18
26 weeks ahead: 2,963 req/s тЖТ N 15 ┬╖ N+2 17 ┬╖ zone-safe 24
52 weeks ahead: 4,204 req/s тЖТ N 21 ┬╖ N+2 23 ┬╖ zone-safe 33
target utilisation 50% тЖТ N = 20 for 2,963 req/s
target utilisation 70% тЖТ N = 15 for 2,963 req/s
target utilisation 90% тЖТ N = 11 for 2,963 req/s

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

рддреНрдпрд╛рдЪ forecast рд╕рд╛рдареА, zone-loss рдЖрд░рд╛рдЦрдбреНрдпрд╛рд▓рд╛ (24) N+2 (17) рдкреЗрдХреНрд╖рд╛ 7 servers рдЬрд╛рд╕реНрдд рд▓рд╛рдЧрддрд╛рдд тАФ рддреБрдореНрд╣реА рдХреЛрдгрддреНрдпрд╛ рдмрд┐рдШрд╛рдбрд╛рд╕рд╛рдареА design рдХрд░рддрд╛ рддреЗ рд╡рд╛рдвреАрдкреЗрдХреНрд╖рд╛ рдЬрд╛рд╕реНрдд рдЦрд░реНрдЪ рдард░рд╡рддреЗ. Utilisation target рдореБрд│реЗ N 11 рдкрд╛рд╕реВрди 20 рдкрд░реНрдпрдВрдд рдмрджрд▓рддреЛ: рддреА headroom рдЪреА рдХрд┐рдВрдордд рдЖрд╣реЗ, рдЖрдгрд┐ рддреА рдирд┐рд╡рдб рдЖрд╣реЗ, рдирд┐рдпрдо рдирд╛рд╣реА. рдЖрдгрд┐ рддреБрдореНрд╣реА рдЬрд┐рддрдХрд╛ рдкреБрдврдЪрд╛ forecast рдХрд░рддрд╛, рддрд┐рддрдХрд╛ рд╕рд░рд│-рд░реЗрд╖реЗрдЪрд╛ рдЕрдВрджрд╛рдЬ рдорд╣рддреНрддреНрд╡рд╛рдЪрд╛ рдард░рддреЛ: рджрд░ рдорд╣рд┐рдиреНрдпрд╛рд▓рд╛ рдкреБрдиреНрд╣рд╛ fit рдХрд░рд╛ рдЖрдгрд┐ рдорд╛рдЧрдЪреНрдпрд╛ рдорд╣рд┐рдиреНрдпрд╛рдЪреНрдпрд╛ forecast рдЪреА рдкреНрд░рддреНрдпрдХреНрд╖ рдШрдбрд▓реЗрд▓реНрдпрд╛рд╢реА рддреБрд▓рдирд╛ рдХрд░рд╛.

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

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

Per-server limit рд╢реЛрдзрдгреНрдпрд╛рд╕рд╛рдареА load-test рдХрд░рд╛ тАФ рдЙрджрд╛рд╣рд░рдгрд╛рд░реНрде k6 рдиреЗ. рдЦрд▒реНрдпрд╛ account рд╡рд░ (test environment, рд╕рд╣рдорддреАрд╢рд┐рд╡рд╛рдп production рдХрдзреАрдЪ рдирд╛рд╣реА):

k6 run --vus 300 --duration 10m load-timetable.js    # raise VUs until p99 or errors break the SLO

Kubernetes рд╡рд░ replicas zones рдордзреНрдпреЗ рдкрд╕рд░рд╡рд╛ рдЖрдгрд┐ maintenance рджрд░рдореНрдпрд╛рди рддреНрдпрд╛рдВрдЪреЗ рд░рдХреНрд╖рдг рдХрд░рд╛:

# in the Deployment's pod template
topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: topology.kubernetes.io/zone
    whenUnsatisfiable: DoNotSchedule
    labelSelector: {matchLabels: {app: timetable}}
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata: {name: timetable}
spec:
  minAvailable: 22            # voluntary disruptions (node drains) may not go below this
  selector: {matchLabels: {app: timetable}}

рдард╛рдКрдХ рдЕрд╕рд▓реЗрд▓реНрдпрд╛ event рд╕рд╛рдареА рдЖрдзреАрдЪ scale рдХрд░рд╛, рдЙрджрд╛рд╣рд░рдгрд╛рд░реНрде Auto Scaling group рд╡рд░реАрд▓ AWS scheduled action рдиреЗ:

aws autoscaling put-scheduled-update-group-action --auto-scaling-group-name timetable \
  --scheduled-action-name results-day --start-time 2026-10-15T04:00:00Z --min-size 36 --desired-capacity 36

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ рдЖрд╣реЗ: рдкреНрд░рддреНрдпреЗрдХ service рд╕рд╛рдареА рдПрдХ рдУрд│ рд▓рд┐рд╣рд╛: 6 рдорд╣рд┐рдиреНрдпрд╛рдВрдирдВрддрд░рдЪрд╛ forecast peak, load test рдордзреВрди per-server limit, target utilisation, N, рдЖрдгрд┐ zone-loss рдЖрдХрдбрд╛. рддреНрдпрд╛рдЪреНрдпрд╛ рд╢реЗрдЬрд╛рд░реА рдкреБрдврдЪреНрдпрд╛ рдард╛рдКрдХ рдЕрд╕рд▓реЗрд▓реНрдпрд╛ event рдЪреА рддрд╛рд░реАрдЦ рд▓рд┐рд╣рд╛.

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

рдкреБрд░реЗрд╢реА capacity рдЕрд╕рд▓реА рддрд░реА рдЧреЛрд╖реНрдЯреА рдмрд┐рдШрдбрддрд╛рдд. рддреЗрд╡реНрд╣рд╛ рдЬрдмрд╛рдмрджрд╛рд░реА рдХреЛрдгрд╛рдЪреА? рдкреБрдврдЪрд╛ рдзрдбрд╛ рдЖрд╣реЗ рдкрдердХрд╛рдЪреЗ incident command.

git checkout lesson-08-incident-command

ЁЯУИ Lesson 07 тАФ Capacity planning: chairs for next term, before the pupils arrive

ЁЯУН You are here: Lesson 07 of 12 ┬╖ Previous: lesson-06-progressive-delivery ┬╖ Next: lesson-08-incident-command


ЁЯУж What's in this branch

Lesson 06, plus planning. Fit the growth from real peaks, forecast it, keep headroom, add spare servers for failures (N+1, N+2), survive the loss of a whole zone, and plan known events (results day) separately. CapacityPlan in sre/crew.py and capacity() in sre/demo.py. How to measure load and autoscale is taught in the Scaling school; this lesson is the crew's plan.

ЁЯзТ Explain like I'm 5

Every week Aishwarya counts how many pupils are in the hall at the busiest moment. ЁЯкС Week 1: 1,206. Week 12: 1,707. It goes up about 48 a week.

"Where will it be in six months?" She draws a straight line through the counts and follows it: about 2,963.

One row of chairs seats 300, but a full row is uncomfortable, so she plans each row for 210 (70% full). That is 15 rows.

"And if one row breaks?" Add one: 16. "If one row is being repaired and another breaks?" Add two: 17. "If the whole east wing of the hall is closed?" The other two wings must seat everyone: 24 rows, spread over three wings.

And results day is different: 2.5 ├Ч the normal crowd. That needs 36 rows тАФ so they borrow chairs before that day, not while the parents are standing.

ЁЯЧ║я╕П Diagram

flowchart LR
    peaks["ЁЯУК 12 weekly peaks<br/>1,206 тЖТ 1,707 req/s"] --> fit["ЁЯУИ straight line<br/>+47.8 req/s per week"]
    fit --> fc["forecast +26 weeks<br/>2,963 req/s"]
    fc --> n["N = 15<br/>210 req/s each (70% of 300)"]
    n --> n1["N+1 = 16"]
    n --> n2["N+2 = 17"]
    n --> z["lose 1 of 3 zones: 24"]
    fc --> ev["results day ├Ч 2.5<br/>7,408 req/s тЖТ 36"]

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

тЭУ What

ЁЯдФ Why

Because running out of capacity is an outage you could see coming for months. And buying far too much is money wasted every month. The plan puts numbers on both, so "we need 9 more servers by March" is an argument with evidence. It also exposes the hidden requirement: the failure plan (a zone down) usually needs more than the growth plan.

ЁЯФз How (in this repo)

CapacityPlan(per_server_rps=300, target_util=0.7, zones=3) in sre/crew.py. weekly_peaks() makes 12 seeded weekly peaks (growth plus noise), fit(ys) returns the least-squares slope and intercept, servers(peak) is тМИpeak ├╖ (300 ├Ч 0.7)тМЙ and zone_safe(n) is 3 ├Ч тМИn ├╖ 2тМЙ. capacity() forecasts 26 weeks ahead of week 12.

ЁЯзк Try it

python3 sre/demo.py capacity
python3 - <<'EOF'
import sys; sys.path.insert(0, "sre"); from crew import CapacityPlan
plan = CapacityPlan()
peaks = plan.weekly_peaks(12); slope, icpt = plan.fit(peaks)
for ahead in (4, 13, 26, 52):
    fc = round(icpt + slope * (11 + ahead)); n = plan.servers(fc)
    print(f"{ahead:>2} weeks ahead: {fc:>5,} req/s тЖТ N {n:>2} ┬╖ N+2 {n + 2:>2} ┬╖ zone-safe {plan.zone_safe(n):>2}")
for util in (0.5, 0.7, 0.9):
    print(f"target utilisation {util:.0%} тЖТ N = {CapacityPlan(target_util=util).servers(2963)} for 2,963 req/s")
EOF

тЬЕ Verify тАФ what you should see

capacity prints:

тФАтФА weekly peak requests/s, 12 weeks: [1206, 1226, 1276, 1373, 1365, 1448, 1474, 1533, 1600, 1613, 1684, 1707]
   straight-line fit: +47.8 req/s per week ┬╖ forecast 26 weeks ahead: 2,963 req/s
   one server: 300 req/s at its load-test limit; plan at 70% = 210 тЖТ N = 15 servers for the forecast peak
   N+1 = 16 (one server can fail) ┬╖ N+2 = 17 (one in maintenance AND one fails) ┬╖ survive losing 1 of 3 zones: 24
   results day (2.5 ├Ч the normal peak = 7,408 req/s) needs 36 тАФ plan it as an event, pre-scale, don't wait for autoscaling

Your snippet prints:

 4 weeks ahead: 1,912 req/s тЖТ N 10 ┬╖ N+2 12 ┬╖ zone-safe 15
13 weeks ahead: 2,342 req/s тЖТ N 12 ┬╖ N+2 14 ┬╖ zone-safe 18
26 weeks ahead: 2,963 req/s тЖТ N 15 ┬╖ N+2 17 ┬╖ zone-safe 24
52 weeks ahead: 4,204 req/s тЖТ N 21 ┬╖ N+2 23 ┬╖ zone-safe 33
target utilisation 50% тЖТ N = 20 for 2,963 req/s
target utilisation 70% тЖТ N = 15 for 2,963 req/s
target utilisation 90% тЖТ N = 11 for 2,963 req/s

ЁЯПБ What you just proved

For the same forecast, the zone-loss plan (24) needs 7 more servers than N+2 (17) тАФ the failure you design for decides the bill more than growth does. The utilisation target moves N from 11 to 20: that is the price of headroom, and it is a choice, not a law. And the further ahead you forecast, the more the straight-line guess matters: re-fit every month and compare last month's forecast with what really happened.

тЪая╕П Common mistakes

ЁЯПн In production

Load-test to find the per-server limit тАФ for example with k6. On a real account (a test environment, never production without agreement):

k6 run --vus 300 --duration 10m load-timetable.js    # raise VUs until p99 or errors break the SLO

Spread replicas across zones and protect them during maintenance on Kubernetes:

# in the Deployment's pod template
topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: topology.kubernetes.io/zone
    whenUnsatisfiable: DoNotSchedule
    labelSelector: {matchLabels: {app: timetable}}
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata: {name: timetable}
spec:
  minAvailable: 22            # voluntary disruptions (node drains) may not go below this
  selector: {matchLabels: {app: timetable}}

Pre-scale for a known event, for example with an AWS scheduled action on an Auto Scaling group:

aws autoscaling put-scheduled-update-group-action --auto-scaling-group-name timetable \
  --scheduled-action-name results-day --start-time 2026-10-15T04:00:00Z --min-size 36 --desired-capacity 36

ЁЯПн Why this matters in production: write one line per service: forecast peak in 6 months, per-server limit from a load test, target utilisation, N, and the zone-loss number. Put the date of the next known event next to it.

тПня╕П Next

Even with enough capacity, things break. When they do, who is in charge? The next lesson is the crew's incident command.

git checkout lesson-08-incident-command
тЖР Previousprogressive deliveryNext тЖТincident command

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