| name | capacity-planning |
| description | Plan infrastructure capacity for current and future load. Outputs resource projections, scaling thresholds, cost forecasts, and capacity headroom recommendations. |
| argument-hint | ["service type","current utilisation","growth rate","peak patterns"] |
| allowed-tools | Read, Write, Bash |
Capacity Planning
Capacity planning prevents two failure modes: running out of capacity (outage) and over-provisioning (wasted budget). It requires knowing your current utilisation, your growth trajectory, your scaling behaviour, and your traffic patterns.
Process
- Baseline current utilisation. CPU, memory, disk, network, DB connections — at p50, p95, and peak.
- Model growth. Historical growth rate + business forecast. Project 3, 6, 12 months.
- Identify scaling constraints. What breaks first as load grows? Stateless services scale horizontally; databases, queues, and storage need different strategies.
- Define capacity thresholds. Target utilisation ceiling (typically 60-70% CPU, 80% memory) to maintain headroom for spikes.
- Run load tests. Confirm the system behaves as expected at 2× and 5× current load.
- Calculate cost projections. Align capacity plan with budget cycle.
- Set automated scaling. HPA, ASG, database read replicas — capacity responds to load automatically where possible.
- Review quarterly. Actual vs forecast. Adjust the model.
Baseline Metrics Collection
kubectl top pods -n production --sort-by=cpu
kubectl top nodes
kubectl get pods -n production -o json | python3 -c "
import json, sys
pods = json.load(sys.stdin)['items']
for p in pods:
for c in p['spec']['containers']:
r = c.get('resources', {})
print(f\"{p['metadata']['name']}/{c['name']}: \
cpu_req={r.get('requests',{}).get('cpu','?')} \
mem_req={r.get('requests',{}).get('memory','?')} \
cpu_lim={r.get('limits',{}).get('cpu','?')}\")
"
psql -c "
SELECT count(*) as active,
max_conn,
round(count(*) * 100.0 / max_conn, 1) as pct_used
FROM pg_stat_activity, (SELECT setting::int AS max_conn FROM pg_settings WHERE name='max_connections') mc
WHERE state != 'idle'
GROUP BY max_conn;"
iostat -xz 1 10
sar -n DEV 1 10
Capacity Model (Python)
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
class CapacityModel:
def __init__(self, current_rps: float, current_cpu_pct: float,
current_memory_gb: float, monthly_growth_rate: float):
self.current_rps = current_rps
self.current_cpu_pct = current_cpu_pct
self.current_memory_gb = current_memory_gb
self.monthly_growth = monthly_growth_rate
self.cpu_threshold = 0.70
self.memory_threshold = 0.80
def project(self, months: int) -> pd.DataFrame:
rows = []
for m in range(months + 1):
growth_factor = (1 + self.monthly_growth) ** m
proj_rps = self.current_rps * growth_factor
proj_cpu = self.current_cpu_pct * growth_factor
proj_mem = self.current_memory_gb * growth_factor
replicas_for_cpu = max(1, np.ceil(proj_cpu / (.cpu_threshold * )))
rows.append({
: m,
: datetime.now() + timedelta(days=*m),
: (proj_rps),
: (proj_cpu / replicas_for_cpu, ),
: (proj_mem / replicas_for_cpu, ),
: (replicas_for_cpu),
: (.cpu_threshold * - proj_cpu / replicas_for_cpu, ),
: proj_cpu / replicas_for_cpu > .cpu_threshold * ,
})
pd.DataFrame(rows)
() -> :
{
: .current_rps * peak_multiplier,
: .current_cpu_pct * peak_multiplier,
: (, (np.ceil(
.current_cpu_pct * peak_multiplier / (.cpu_threshold * )
))),
}
model = CapacityModel(
current_rps=,
current_cpu_pct=,
current_memory_gb=,
monthly_growth_rate=,
)
projection = model.project()
(projection[[, , , ]].to_string())
peak = model.peak_capacity(peak_multiplier=)
()
HPA Configuration
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-service-hpa
namespace: production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api-service
minReplicas: 3
maxReplicas: 50
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 75
- type: Pods
pods:
Database Capacity Planning
SELECT
schemaname,
tablename,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS total_size,
pg_size_pretty(pg_relation_size(schemaname||'.'||tablename)) AS data_size,
pg_size_pretty(pg_indexes_size(schemaname||'.'||tablename)) AS index_size
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC
LIMIT 20;
Capacity Planning Report Template
# Capacity Report: [Service Name]
**Period:** Q1 2025 | **Reviewed:** 2025-01-15 | **Next review:** 2025-04-15
## Current State
| Resource | Current | Target Ceiling | Headroom |
|----------|---------|----------------|----------|
| CPU (avg) | 35% | 70% | 35% |
| Memory (avg) | 62% | 80% | 18% |
| DB connections | 45/200 | 160/200 | 77% |
| Disk (data) | 1.2TB | 4TB | 70% |
| RPS (peak) | 1,200 | — | — |
## Growth Forecast
**Historical rate:** 12% MoM
**Business forecast:** 15% MoM (new market launch Q2)
| Month | Projected RPS | Replicas Needed | Est. Cost/mo |
|-------|--------------|-----------------|--------------|
| Now | 1,200 | 6 | $2,400 |
| +3mo | 1,720 | 9 | $3,600 |
| +6mo | 2,460 | 13 | $5,200 |
| +12mo | 5,040 | 26 | $10,400 |
## Scaling Actions Required
- [ ] Increase HPA maxReplicas: 20 → 50 before Q2 launch
- [ ] Add PostgreSQL read replica before +3mo milestone (connections approaching 50%)
- [ ] Upgrade Redis to cluster mode at +6mo (memory projection: 85%)
- [ ] Storage auto-scaling: enable for RDS (projected to reach 75% by +4mo)
## Peak Capacity (Black Friday 5×)
- Replicas needed: 30 (current max: 20) — **ACTION REQUIRED**
- DB read replicas needed: 2 additional
- Estimated peak cost: $8,000/day
Anti-Patterns to Avoid
| Anti-Pattern | Problem | Fix |
|---|
| Planning only at crisis | Capacity gaps discovered during incidents | Quarterly review cadence |
| Ignoring peak patterns | Steady-state planning misses seasonal spikes | Model 3× and 5× peak scenarios |
| No load testing | Model is theoretical; real behaviour differs | Validate projections with load tests |
| Autoscaling without ceiling | Runaway scaling = runaway costs | Always set maxReplicas with cost awareness |
| Scaling before profiling | Adding capacity masks inefficiency | Profile first; sometimes 10× efficiency gain is possible |
| DB connection count ignored | Connection exhaustion before CPU limit | Model connections as a capacity dimension |
| Annual planning only | Business changes faster than annual cycles | Monthly data review, quarterly formal planning |
10 Rules
- Establish baselines before projecting — you cannot plan from intuition alone.
- Growth models include both organic and event-driven (launch, campaigns, seasonal) projections.
- Plan to 70% CPU utilisation ceiling — 30% headroom absorbs spikes without incident.
- Database capacity is a separate dimension from compute — connections, IOPS, storage growth.
- Autoscaling is not a substitute for capacity planning — maxReplicas must be set deliberately.
- Load test at 2× and 5× current load before every major launch.
- Cost is part of the capacity plan — capacity without budget approval is just a wish.
- Review actual vs forecast every quarter — update the growth model if reality diverges.
- Plan peak capacity separately — Black Friday is not the same problem as daily steady state.
- Start scaling actions 4–6 weeks before projected threshold is reached — lead time matters.