Kubernetes Autoscaling skill for Claude Code
Scale on the right signal, before latency breaks. Has Claude treat autoscaling as one system: requests set from measured usage, a scaling signal that leads saturation, HPA behavior tuned against flapping, KEDA for queues and scale to zero, and Karpenter or Cluster Autoscaler sized to provisioning time. It keeps VPA and HPA off the same metric, writes PDBs that let nodes drain, and ships a simulator that replays a load curve through the HPA math.
- name
kubernetes-autoscaling - size331 lines · ≈3,900 tokens
- files2
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Designs and tunes Kubernetes autoscaling end to end - HPA autoscaling/v2 metrics, targets, tolerance and behavior stabilization, VPA modes including in-place resize and its conflict with HPA, KEDA ScaledObjects for queues and event-driven scale to zero, node autoscaling with Cluster Autoscaler or Karpenter, requests as the prerequisite, PodDisruptionBudgets that do not block scale-down, and load-testing the policy before production. Use when pods do not scale or scale too late, replicas flap, nodes stay underused, a queue backlog grows, or when adding HPA, VPA, KEDA or Karpenter to a cluster. Try asking
- Our API pods scale up too late during traffic spikes, tune the HPA
- Add KEDA so the order worker scales on SQS backlog and down to zero at night
- Replicas keep flapping between 4 and 12 every few minutes, find out why
Use it when
- Adding or tuning an HPA, VPA, or KEDA ScaledObject
- Pods scale too late, too early, or flap
- Nodes stay underused or pending pods wait minutes for capacity
- Choosing between Cluster Autoscaler and Karpenter
- Validating an autoscaling policy before a launch or peak event
Skip it when
- You are writing the Deployment, probes, or Service manifests themselves - use kubernetes-deploy
- You need a long-range forecast of capacity and cost - use capacity-planning
How to check the result
hpa_simulate.py shows zero saturated seconds on a production load curve, and a staging ramp test keeps p95 within the SLO with no long-pending pods and no errors on scale-down.
The common mistake it prevents
Running VPA and HPA on CPU for the same Deployment: VPA raises requests, utilization falls, the HPA scales in, and the two controllers fight indefinitely.
Autoscaling is done when every scaled workload has requests set from measured usage, scales on the signal that actually precedes saturation, adds capacity faster than load arrives (pods and nodes), removes it without dropping requests, and the policy has been replayed against a real load curve before production.
First, inspect
kubectl version # server minor matters for feature states
kubectl get hpa,vpa,scaledobject,scaledjob,pdb -A 2>/dev/null
kubectl describe hpa <name> -n <ns> # conditions + events explain every decision
kubectl top pods -n <ns> --containers # needs metrics-server
kubectl get apiservice | grep -E 'metrics.k8s.io|custom.metrics|external.metrics'
kubectl get nodepools,nodeclaims 2>/dev/null || kubectl -n kube-system get deploy cluster-autoscaler
In kubectl describe hpa, read the conditions: AbleToScale, ScalingActive (false
means metrics are missing), ScalingLimited (pinned at min or max). Events show each
rescale and why.
Workflow
- [ ] 1. Measure: per-pod throughput at the latency SLO, startup time, node provisioning time
- [ ] 2. Set requests from observed usage (VPA in Off mode or metrics history)
- [ ] 3. Pick the scaling signal (table below) and the target per pod
- [ ] 4. Configure HPA or KEDA with min/max and behavior
- [ ] 5. Configure node autoscaling so pending pods get nodes fast enough
- [ ] 6. Add PDBs and graceful shutdown so scale-down is safe
- [ ] 7. Replay a load curve (scripts/hpa_simulate.py) and run a real load test
- [ ] 8. Alert on pinned-at-max, pending pods, and flapping
Prerequisite: requests
- HPA
Utilizationtargets are a percentage of the container request. A pod whose containers lack the relevant request has undefined utilization, and the HPA takes no action for that metric. - Set CPU requests near steady-state p90 usage and memory requests near peak working set (kubernetes-deploy covers sizing and limits in depth).
- Get recommendations without changing anything: a VPA with
updateMode: "Off"andkubectl describe vpa <name>(target, lowerBound, upperBound).
Choose the signal
| Workload | Scale on | Tool |
|---|---|---|
| CPU-bound HTTP/gRPC service | CPU utilization, target 60-75% of request | HPA Resource or ContainerResource |
| I/O-bound or async service where CPU stays low | Requests per second or in-flight requests per pod | HPA Pods/Object metric via an adapter, or KEDA Prometheus trigger |
| Queue consumers (SQS, Kafka, RabbitMQ, Pub/Sub, Redis streams) | Backlog per replica (lag / messages per pod) | KEDA ScaledObject |
| Run-to-completion work items | One Job per batch of messages | KEDA ScaledJob |
| Idle most of the day, must scale to zero | Event source with activation threshold | KEDA (minReplicaCount: 0) |
| Memory-bound with steady load | Usually right-size with VPA instead of scaling out | VPA |
| Stateful single-writer | Do not autoscale horizontally | VPA recommendations, manual |
Memory is a poor HPA signal for most runtimes: usage does not fall when load does (JVM heaps, caches), so replicas ratchet up and never come down.
HPA (autoscaling/v2)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: { name: api, namespace: shop }
spec:
scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: api }
minReplicas: 3 # survive one zone loss and absorb the first spike
maxReplicas: 40 # bounded by DB connections and budget
metrics:
- type: ContainerResource # ignore sidecar CPU
containerResource:
name: cpu
container: app
target: { type: Utilization, averageUtilization: 65 }
behavior:
scaleUp:
stabilizationWindowSeconds: 0
policies:
- { type: Percent, value: 100, periodSeconds: 15 }
- { type: Pods, value: 4, periodSeconds: 15 }
selectPolicy: Max
scaleDown:
stabilizationWindowSeconds: 300
policies:
- { type: Percent, value: 20, periodSeconds: 60 }
How it decides:
desiredReplicas = ceil(currentReplicas * currentMetric / targetMetric), evaluated every 15 s (--horizontal-pod-autoscaler-sync-period).- No change while the ratio is within the tolerance (default 0.1, so 59-71% does
nothing at a 65% target). Per-HPA
toleranceunderbehavior.scaleUp/behavior.scaleDownis stable as of Kubernetes 1.37 (introduced 1.33 behindHPAConfigurableTolerance); on older clusters it may be unavailable. - With several metrics, the HPA computes a count per metric and uses the largest.
- Not-yet-ready pods are set aside; during scale-up they are assumed to use 0%, which dampens the step. Slow readiness therefore slows scaling; fix startup time first.
- Scale-down uses the highest recommendation within the stabilization window (default 300 s). Scale-up has no window by default and is limited to the larger of +100% or +4 pods per 15 s.
Behavior tuning:
| Symptom | Change |
|---|---|
| Too slow to catch spikes | Lower target (more headroom), raise minReplicas, add a Pods policy allowing bigger steps; cut pod startup time |
| Flapping up and down | Longer scaleDown.stabilizationWindowSeconds; slower scale-down policy (for example 20% per 60 s); check the metric is not noisy |
| Never scales down | Metric does not fall with load (memory), or minReplicas too high, or a stuck high recommendation from a noisy metric |
| Scale-down drops requests | Graceful shutdown: preStop sleep plus terminationGracePeriodSeconds longer than the longest request |
| Never scale down during an incident | behavior.scaleDown.selectPolicy: Disabled temporarily |
Rules:
- Remove
spec.replicasfrom the Deployment manifest once an HPA owns it, or everykubectl applyresets the count. Removing it is a one-time scale to 1 unless done carefully; apply when the HPA is already active and at low traffic. - One autoscaler per workload: never an HPA and a KEDA ScaledObject on the same Deployment (KEDA creates its own HPA).
maxReplicasmust respect downstream limits: replicas x pool size must stay under the database connection budget.- Custom and external metrics need an adapter. Only one service can serve
external.metrics.k8s.ioper cluster; KEDA and prometheus-adapter conflict there.
VPA
updateMode | Behavior | Use |
|---|---|---|
Off | Recommendations only | Default starting point; feed requests in your manifests |
Initial | Sets requests at pod creation only | Batch and jobs |
InPlaceOrRecreate | Resizes running pods in place, evicts if it cannot (GA in VPA 1.6; needs Kubernetes 1.33+, in-place resize is GA in 1.35) | Long-running services that tolerate occasional restarts |
InPlace | In-place only, never evicts (alpha, VPA 1.7) | Disruption-intolerant workloads, once stable |
Recreate | Evicts pods to apply new requests | Rarely; respects PDBs |
Auto | Deprecated alias of Recreate | Replace with an explicit mode |
- Never run VPA and HPA on the same resource metric (CPU or memory) for one workload:
VPA raises requests, utilization drops, HPA scales in, and they fight. Allowed
combinations: HPA on a custom or external metric (RPS, queue lag) plus VPA on CPU and
memory, or VPA in
Offmode alongside any HPA. - Set
minAllowedandmaxAllowedinresourcePolicyso a bad recommendation cannot request a whole node or starve the pod. - VPA is not compatible with pod-level
resourcesstanzas yet.
KEDA
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata: { name: order-worker, namespace: shop }
spec:
scaleTargetRef: { name: order-worker }
minReplicaCount: 0
maxReplicaCount: 50
pollingInterval: 30 # default 30 s
cooldownPeriod: 300 # only governs the last replica -> 0
fallback: { failureThreshold: 3, replicas: 5 }
advanced:
horizontalPodAutoscalerConfig:
behavior:
scaleDown: { stabilizationWindowSeconds: 120 }
triggers:
- type: aws-sqs-queue
authenticationRef: { name: aws-workload-identity }
metadata:
queueURL: https://sqs.us-east-1.amazonaws.com/123456789012/orders
queueLength: "20" # target messages per replica
activationQueueLength: "1" # wake from zero
awsRegion: us-east-1
- KEDA handles 0 <-> 1 (activation); the HPA it creates handles 1 <-> N.
cooldownPerioddoes not slow normal scale-down; tune that throughhorizontalPodAutoscalerConfig. - Target per replica = what one replica drains in your latency budget. If a pod handles
10 messages/s and backlog must clear within 60 s,
queueLengthnear 600 is the ceiling, lower for headroom. - Always set
fallback, or a broken metric source (expired credentials, Prometheus down) leaves the workload at whatever it had. - Long jobs that must not be killed mid-item: use ScaledJob, or set
terminationGracePeriodSecondsabove the longest item and make work idempotent. - Scale to zero means a cold start on the first event; measure it and confirm the latency is acceptable before enabling for user-facing paths.
- Native HPA
minReplicas: 0(HPAScaleToZero, beta and on by default in 1.37) works only with Object or External metrics; KEDA remains the portable choice.
Node autoscaling
Pods scale in seconds; nodes take minutes. Total reaction time = metric delay + HPA sync + pod scheduling + node provisioning + image pull + app startup.
| Situation | Choose |
|---|---|
| EKS, or a cloud with a Karpenter provider (AWS, Azure) | Karpenter: provisions right-sized nodes per pending pod, consolidates underused nodes |
| GKE | Built-in cluster autoscaler or node auto-provisioning |
| Other clouds, fixed node group shapes | Cluster Autoscaler with node groups |
Karpenter (v1 API):
apiVersion: karpenter.sh/v1
kind: NodePool
metadata: { name: general }
spec:
template:
spec:
nodeClassRef: { group: karpenter.k8s.aws, kind: EC2NodeClass, name: default }
requirements:
- { key: karpenter.sh/capacity-type, operator: In, values: ["spot", "on-demand"] }
- { key: kubernetes.io/arch, operator: In, values: ["arm64", "amd64"] }
- { key: karpenter.k8s.aws/instance-category, operator: In, values: ["c", "m", "r"] }
expireAfter: 720h
limits: { cpu: "2000" }
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 2m
budgets:
- nodes: "10%"
- nodes: "0"
schedule: "0 9 * * mon-fri"
duration: 8h
reasons: ["Underutilized"]
consolidateAfterdefaults to0s; a few minutes avoids churn after every deploy. Recent releases also offerconsolidationPolicy: Balancedto skip low-value moves.- Default budget is
nodes: 10%. Block consolidation during business hours with a scheduled budget if churn hurts. - Protect a pod from voluntary disruption with the annotation
karpenter.sh/do-not-disrupt: "true"(or a duration such as"30m"). - Set
limitson every NodePool; without them a runaway HPA buys nodes without bound.
Cluster Autoscaler: scale-down after --scale-down-unneeded-time (default 10m) below
--scale-down-utilization-threshold (default 0.5 of requests); use
--balance-similar-node-groups across zones and an --expander (least-waste or
priority). Pods with cluster-autoscaler.kubernetes.io/safe-to-evict: "false" block
their node’s removal.
For instant headroom, run a low-priority placeholder Deployment (negative
PriorityClass) that real pods preempt while new nodes come up.
PodDisruptionBudgets
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata: { name: api, namespace: shop }
spec:
maxUnavailable: 1 # or a percentage; scales with replica count
unhealthyPodEvictionPolicy: AlwaysAllow
selector: { matchLabels: { app: api } }
- Prefer
maxUnavailableoverminAvailable;minAvailableequal to the replica count (or100%) blocks every node drain and consolidation forever. unhealthyPodEvictionPolicy: AlwaysAllowlets drains evict crash-looping pods that would otherwise block them.- Single-replica workloads with a PDB of
maxUnavailable: 0block node scale-down; run two replicas or accept the disruption. - PDBs cover voluntary evictions only; they do not protect against node failure.
Test the policy
- Replay a production load curve through the HPA math before touching the cluster:
# load.csv: t_seconds,load (total CPU millicores, RPS, or queue rate across all pods)
python3 scripts/hpa_simulate.py load.csv --target 650 --min 3 --max 40 \
--ready-delay 45 --capacity 900
python3 scripts/hpa_simulate.py load.csv --target 650 --min 3 --max 40 \
--ready-delay 45 --down-window 600 --down-percent 20 --down-period 60 --capacity 900
It prints each rescale, peak replicas, flaps, pod-hours (cost proxy), and seconds where
per-pod load exceeded the target and the measured --capacity (exit 1 if saturated).
Add node provisioning time to --ready-delay when nodes must be created. Compare
policies on the same curve; pick the one with zero saturated seconds at the lowest
pod-hours.
- Run a real ramp test (k6, Locust, or Vegeta) in staging at production shape: a step from baseline to 3-5x within one minute, a hold, and a drop. Watch:
kubectl get hpa api -n shop -w
kubectl get pods -n shop -l app=api -w
kubectl get nodeclaims -w # Karpenter
Pass criteria: p95 latency stays within the SLO during the ramp, no pod stays Pending longer than the measured node provisioning time, scale-down completes without 5xx, and replicas do not oscillate after the drop.
Alerts
- HPA at
maxReplicasfor more than 15 minutes (ScalingLimitedtrue) while the metric is above target. - Pods Pending longer than expected node provisioning time.
- More than N rescale events per hour (flapping).
- KEDA scaler errors or fallback active.
Gotchas
- metrics-server reports CPU averaged over its scrape window; very short spikes never reach the HPA. Scale on a leading signal (RPS, queue length) for bursty traffic.
- Init containers and sidecars count toward Pod-level
Resourcemetrics; useContainerResourcefor the app container. - CPU limits cause throttling that looks like low utilization at high latency; the HPA then under-scales. Prefer no CPU limit (or a generous one) for latency-sensitive pods.
- JVM and other slow-starting apps: use a
startupProbeand keep readiness honest, or the HPA counts cold pods as capacity. - Changing
maxReplicasor node limits in an incident without checking the database connection budget moves the outage downstream. - Spot interruptions are involuntary disruptions; spread across instance types and
zones and keep enough on-demand baseline for
minReplicas.
Output
**Workload:** shop/api (Deployment), HPA on container CPU 65%, min 3 / max 40
**Behavior:** up +100% or +4 pods per 15 s; down 20% per 60 s after a 300 s window
**Nodes:** Karpenter NodePool general (spot + on-demand, c/m/r, arm64+amd64), consolidateAfter 2m, limit 2000 CPU
**Safety:** PDB maxUnavailable 1 (AlwaysAllow), preStop 10 s, grace 40 s
**Simulation:** Black Friday curve: 0 s saturated, peak 31 replicas, 182 pod-hours (vs 214 with defaults)
**Load test:** 4x step in 60 s, p95 180 ms (SLO 300 ms), max Pending 70 s, no 5xx on scale-down Bundled files
Loaded only when a step in the skill needs them. Review them here before installing.
scripts/hpa_simulate.py
#!/usr/bin/env python3
"""Replay a load curve through an approximation of the HPA (autoscaling/v2) algorithm.
Usage:
python3 scripts/hpa_simulate.py load.csv --target 600 [options]
load.csv has a header and two columns: t_seconds,load
load = total demand in the metric's unit across all pods, e.g. total CPU millicores
used, or total requests per second. Values hold until the next row (step function).
--target is the per-pod target in the same unit (e.g. 600 = 60% of a 1000m CPU request,
or 50 = 50 RPS per pod for an AverageValue metric).
Modeled: the core formula ceil(replicas * current/target), tolerance, not-yet-ready pods
counted at zero usage during scale-up, scale-up and scale-down stabilization windows,
Percent and Pods rate policies with selectPolicy Max, min/max replicas, pod readiness
delay. Not modeled: multiple metrics, missing metrics, node provisioning time (add it to
--ready-delay), VPA, KEDA activation. Treat the output as a policy comparison tool, not
an exact prediction.
Options (defaults match Kubernetes defaults):
--min 1 --max 10 --initial <min> replica bounds and starting replicas
--sync 15 controller sync period, seconds
--tolerance 0.1 no action while |ratio - 1| <= tolerance
--ready-delay 30 seconds from pod creation to ready (add node startup if needed)
--up-window 0 --down-window 300 stabilizationWindowSeconds
--up-percent 100 --up-pods 4 --up-period 15
--down-percent 100 --down-pods 0 --down-period 15 (0 disables that policy)
--capacity X per-pod load at which latency breaks (from a load test);
reports seconds above it and exits 1 if any
--every N print a row every N seconds (default: on change)
Exit codes: 0 = simulated (and never above --capacity), 1 = saturation seen, 2 = bad input.
"""
from __future__ import annotations
import argparse
import csv
import math
import sys
def load_curve(path: str) -> list[tuple[float, float]]:
rows: list[tuple[float, float]] = []
try:
with open(path, newline="", encoding="utf-8") as fh:
reader = csv.reader(fh)
header = next(reader, None)
if header is None:
raise ValueError("empty file")
for i, row in enumerate(reader, start=2):
if not row or not row[0].strip():
continue
if len(row) < 2:
raise ValueError(f"line {i}: expected t_seconds,load")
rows.append((float(row[0]), float(row[1])))
except (OSError, ValueError) as exc:
print(f"error: {path}: {exc}", file=sys.stderr)
sys.exit(2)
if not rows:
print("error: no data rows", file=sys.stderr)
sys.exit(2)
rows.sort()
return rows
def load_at(curve: list[tuple[float, float]], t: float) -> float:
value = curve[0][1]
for ts, v in curve:
if ts <= t:
value = v
else:
break
return value
def main() -> int:
p = argparse.ArgumentParser(description="HPA policy simulator")
p.add_argument("csv")
p.add_argument("--target", type=float, required=True)
p.add_argument("--min", type=int, default=1)
p.add_argument("--max", type=int, default=10)
p.add_argument("--initial", type=int)
p.add_argument("--sync", type=float, default=15)
p.add_argument("--tolerance", type=float, default=0.1)
p.add_argument("--ready-delay", type=float, default=30)
p.add_argument("--up-window", type=float, default=0)
p.add_argument("--down-window", type=float, default=300)
p.add_argument("--up-percent", type=float, default=100)
p.add_argument("--up-pods", type=int, default=4)
p.add_argument("--up-period", type=float, default=15)
p.add_argument("--down-percent", type=float, default=100)
p.add_argument("--down-pods", type=int, default=0)
p.add_argument("--down-period", type=float, default=15)
p.add_argument("--capacity", type=float)
p.add_argument("--every", type=float)
a = p.parse_args()
if a.target <= 0 or a.min < 0 or a.max < max(a.min, 1) or a.sync <= 0:
print("error: need --target > 0, 0 <= --min <= --max, --max >= 1, --sync > 0", file=sys.stderr)
return 2
curve = load_curve(a.csv)
end = curve[-1][0]
replicas = a.initial if a.initial is not None else max(a.min, 1)
replicas = min(max(replicas, a.min), a.max)
pods = [-a.ready_delay] * replicas # creation times; initial pods already ready
history: list[tuple[float, int]] = [] # (t, raw recommendation)
events: list[tuple[float, int]] = [] # (t, delta replicas)
stats = {"pod_seconds": 0.0, "above_target_s": 0.0, "saturated_s": 0.0, "peak": replicas,
"scale_ups": 0, "scale_downs": 0, "flaps": 0}
last_dir = 0
last_dir_t = -1e9
last_print = None
prev_state = None
print(f"{'t':>7} {'load':>10} {'ready/total':>11} {'per_pod':>9} {'raw':>4} {'desired':>7}")
t = 0.0
while t <= end:
load = load_at(curve, t)
ready = sum(1 for c in pods if t - c >= a.ready_delay)
total = len(pods)
not_ready = total - ready
# raw recommendation (HPA formula with readiness handling)
if ready == 0:
raw = replicas
per_pod = float("inf") if load > 0 else 0.0
else:
per_pod = load / ready
ratio = per_pod / a.target
if abs(ratio - 1) <= a.tolerance:
raw = replicas
elif ratio > 1 and not_ready > 0:
ratio2 = (load / total) / a.target # not-ready pods assumed at 0 usage
raw = replicas if (ratio2 <= 1 or abs(ratio2 - 1) <= a.tolerance) else math.ceil(ratio2 * total)
else:
raw = math.ceil(ratio * ready)
history.append((t, raw))
keep = max(a.up_window, a.down_window)
history = [(ht, r) for ht, r in history if t - ht <= keep]
# stabilization: up uses min over up window, down uses max over down window
up_rec = min(r for ht, r in history if t - ht <= a.up_window)
down_rec = max(r for ht, r in history if t - ht <= a.down_window)
desired = replicas
if desired < up_rec:
desired = up_rec
if desired > down_rec:
desired = down_rec
# rate policies (selectPolicy Max = most permissive)
if desired > replicas:
added = sum(d for et, d in events if d > 0 and t - et < a.up_period)
start = replicas - added
limits = []
if a.up_percent > 0:
limits.append(math.ceil(start * (1 + a.up_percent / 100)))
if a.up_pods > 0:
limits.append(start + a.up_pods)
if limits:
desired = min(desired, max(limits))
elif desired < replicas:
removed = -sum(d for et, d in events if d < 0 and t - et < a.down_period)
start = replicas + removed
limits = []
if a.down_percent > 0:
limits.append(math.ceil(start * (1 - a.down_percent / 100)))
if a.down_pods > 0:
limits.append(start - a.down_pods)
if limits:
desired = max(desired, min(limits))
desired = min(max(desired, a.min), a.max)
if desired != replicas:
delta = desired - replicas
direction = 1 if delta > 0 else -1
if last_dir and direction != last_dir and t - last_dir_t < 600:
stats["flaps"] += 1
last_dir, last_dir_t = direction, t
stats["scale_ups" if delta > 0 else "scale_downs"] += 1
events.append((t, delta))
events = [(et, d) for et, d in events if t - et < max(a.up_period, a.down_period)]
if delta > 0:
pods.extend([t] * delta)
else:
pods.sort() # remove newest (not-ready first)
del pods[len(pods) + delta:]
replicas = desired
stats["peak"] = max(stats["peak"], replicas)
changed = (raw, replicas, ready) != prev_state
prev_state = (raw, replicas, ready)
show = last_print is None or (a.every and t - last_print >= a.every) or (not a.every and changed)
if show:
pp = "inf" if per_pod == float("inf") else f"{per_pod:.1f}"
print(f"{t:>7.0f} {load:>10.1f} {ready:>5}/{total:<5} {pp:>9} {raw:>4} {replicas:>7}")
last_print = t
stats["pod_seconds"] += len(pods) * a.sync
if per_pod > a.target * (1 + a.tolerance):
stats["above_target_s"] += a.sync
if a.capacity is not None and per_pod > a.capacity:
stats["saturated_s"] += a.sync
t += a.sync
print()
print(f"peak replicas {stats['peak']}")
print(f"scale ups / downs {stats['scale_ups']} / {stats['scale_downs']}")
print(f"direction flips <10m {stats['flaps']}")
print(f"pod-hours {stats['pod_seconds'] / 3600:.2f}")
print(f"above target {stats['above_target_s']:.0f} s (per-ready-pod load > target + tolerance)")
if a.capacity is not None:
print(f"saturated {stats['saturated_s']:.0f} s (per-ready-pod load > capacity {a.capacity:g})")
if stats["saturated_s"] > 0:
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.