Kubernetes HPA Generator

Build a HorizontalPodAutoscaler on autoscaling/v2, with the CPU target, the replica range and both stabilization windows written out. It tells you what the percentage is a percentage of, and says so plainly when the target pods have no CPU request and the autoscaler therefore cannot work at all.

Autoscaler
Replica range
Metrics

Every target here is an average across the ready pods, not a per-pod ceiling.

Scaling behaviour

Written out explicitly because these are the two numbers people assume are symmetric and are not.

Check it against your workload

Not part of the output. Used to tell you whether the CPU metric can be computed at all.

hpa.yaml

updates as you type

    Common mistakes

    These are the ones that fail silently. The config is accepted, nothing raises an error, and the consequence arrives later.

    1. Not setting resource requests

      The HPA computes utilisation as a percentage of the request. With no request there is no denominator, so it reports unknown and never scales.

      Instead:Set CPU and memory requests on every container the HPA targets.

    2. Expecting the HPA to scale on memory usefully

      Most applications do not release memory back, so memory utilisation ratchets up and never comes down, which scales up and never down.

      Instead:Scale on CPU or a custom metric. Memory is rarely a good signal.

    3. Leaving the default stabilisation window

      Scale-down waits five minutes by default, which reads as the HPA being broken when traffic drops.

      Instead:Tune behavior.scaleDown if faster is genuinely wanted, knowing it risks flapping.

    What a HorizontalPodAutoscaler actually measures

    An HPA reads an average metric across the ready pods of one workload, divides it by the target you set, and writes a new replica count to that workload's scale subresource. Everything that goes wrong with one comes from a detail in that sentence: what the percentage is a percentage of, where the metric comes from, which pods count towards the average, and who else is writing to the same field.

    Utilization is a percentage of the request, not the limit

    averageUtilization is measured against resources.requests.cpu on the pod, so a target of 70% on a container requesting 200m means the HPA holds the average near 140m. It has no relationship to the CPU limit and none to the node. The consequence people hit is the one with no error message: a pod with no CPU request gives the controller nothing to divide by, so the metric is unknown, and an HPA with an unknown metric does not scale in either direction. It reports this and carries on, indefinitely.

    requests.cpu: 200m,  target 70%
      -> HPA acts on ~140m average per pod
      -> a pod using 200m reads as 100%
      -> a pod using 400m reads as 200%
    
    requests.cpu absent:
    $ kubectl get hpa
    NAME     TARGETS           MINPODS  MAXPODS  REPLICAS
    api-hpa  <unknown>/70%     2        10       2
             ^ permanent, not "still starting"
    
    Raising the request lowers the percentage for the
    same real load. Requests and targets are tuned
    together or not at all.

    The formula, and the 10% band where nothing happens

    The controller runs every 15 seconds, computes the average over ready pods, and applies one line of arithmetic. It then ignores its own answer if the ratio is within 10% of 1.0, which is the cluster-wide tolerance on kube-controller-manager. So a target that is slightly overshot is not a problem to tune away: it is inside the deadband on purpose, because scaling on a 3% overshoot would mean scaling constantly.

    desiredReplicas =
      ceil(currentReplicas * (currentMetric / targetMetric))
    
    4 replicas, average 105%, target 70%
      ceil(4 * 1.5) = 6 replicas
    
    10 replicas, average 35%, target 70%
      ceil(10 * 0.5) = 5 replicas
    
    tolerance, target 70%:
      63% .. 77%  -> ratio within 10% of 1.0, no action
    
    Pods that are not Ready are excluded from the
    average. Pods with no metric yet count as 0% when
    scaling up and 100% when scaling down, so a missing
    reading never causes a scale event by itself.

    Scale up is immediate, scale down waits five minutes

    behavior.scaleUp.stabilizationWindowSeconds defaults to 0 and behavior.scaleDown.stabilizationWindowSeconds defaults to 300. The asymmetry is deliberate. Being under-provisioned costs requests now, so the controller adds pods on the first reading that asks for it. Removing pods is the dangerous direction, so it takes the highest recommendation from the last five minutes, which means load has to stay down for the whole window before a single pod goes. Shorten the window and the autoscaler follows every dip in traffic, removing pods it wants back a minute later, each cycle paying a pod start and, on a JVM, a cold JIT.

    behavior:
      scaleUp:
        stabilizationWindowSeconds: 0     # default
      scaleDown:
        stabilizationWindowSeconds: 300   # default
    
    scale down, window 300s:
      t+0s   recommendation 4
      t+60s  recommendation 3
      t+120s recommendation 6   <- highest in window
      t+180s recommendation 3
      actual replicas: 6, until the spike ages out
    
    Leave the pair out entirely and you get exactly
    these numbers. They are written into the output so
    the asymmetry is visible rather than assumed.

    An HPA and spec.replicas fight over the same field

    Both write the replica count of the workload. kubectl apply sets it from the manifest, then the controller sets it back within about 15 seconds, and the pods start and stop for as long as anything keeps applying. With a GitOps controller reconciling continuously that is permanent, and it shows up as a workload that will not settle rather than as an error anywhere. The fix is to remove replicas from the Deployment, which is what our Kubernetes YAML generator does whenever it emits an HPA.

    # Deployment: no replicas field at all
    spec:
      selector:
        matchLabels: { app: api }
      template: ...
    
    # HPA owns the count from here on
    spec:
      scaleTargetRef:
        kind: Deployment
        name: api
      minReplicas: 2
      maxReplicas: 10
    
    # The HPA also cannot scale a DaemonSet: it writes
    # to the scale subresource and a DaemonSet has none.

    Multiple metrics take the highest, and memory rarely helps

    Add a second metric and the HPA does not average or combine them. It computes a desired replica count from each metric independently and uses the largest, so a second metric can only ever raise the count. That makes memory a poor second signal for most JVM and Go services: neither returns freed heap to the operating system by default, so the working set rises to its high-water mark and stays there. The HPA scales up on it and then never scales down, because the number never falls. Queue depth or requests per second through an external metric describes load; memory describes the allocator.

    cpu    -> ceil(4 * (90/70))  = 6
    memory -> ceil(4 * (60/80))  = 3
                           result: 6
    
    Two more scoping details:
    
    Resource averages over the SUM of every container
    request in the pod, so an idle sidecar joins the
    denominator and drags the percentage down. Use
    ContainerResource (1.27+, stable in 1.30) to target
    one container instead.
    
    minReplicas: 0 needs the alpha HPAScaleToZero gate
    and an Object or External metric. On CPU it cannot
    work: ceil(0 * anything) is 0, so there is no way
    back up.

    It is not a scheduler, and metrics-server is not installed

    The HPA writes a number and stops. If there is no room in the cluster, the extra pods sit Pending and the HPA still reports the scale as achieved, so a saturated node pool looks like a working autoscaler from the HPA's side. Finding capacity is the cluster autoscaler's or Karpenter's job. The other half of this is the supply of metrics: metrics-server is a separate component that GKE and AKS ship and that kubeadm and EKS clusters do not, and without it there are no resource metrics at all.

    $ kubectl top pods
    error: Metrics API not available
      -> metrics-server is missing. Every HPA in the
         cluster reads <unknown>. Fix this first.
    
    $ kubectl describe hpa api-hpa
      ScalingActive  True   ValidMetricFound
      ScalingLimited True   TooManyReplicas
      -> the HPA wanted more than maxReplicas
    
    $ kubectl get pods
    api-7d9f-x   0/1   Pending
      -> the HPA did its part; there is no node with
         room for the pod it asked for

    More kubernetes tools

    Kubernetes YAML Generator Answer a few questions, get a manifest Kubernetes YAML Validator Is this YAML valid, and where is it wrong? Kubernetes Rollout & Probe Timing Restart loops, capacity dips and stuck drains Kubernetes Service Checker Will this Service have any endpoints? Kubernetes Network Policy Viewer What can reach what, and what just broke Kubernetes Manifest Diff What actually changed Kubernetes Resource Calculator What these manifests actually reserve Kubernetes RBAC Viewer Who can actually do what Kubernetes Deployment Generator See the capacity dip before you deploy Kubernetes Service Generator port, targetPort, and which one you meant Kubernetes StatefulSet Generator Stable names, and volumes that outlive you Kubernetes QoS Class Calculator And where you sit in the eviction order Kubernetes Node Capacity Planner Allocatable is not capacity Kubernetes Max Pods per Node Calculator The ENI limit, not the 110 default Kubernetes Label Selector Tester Which key failed, not just that one did Kubernetes Taint & Toleration Tester Tolerations permit, they do not attract Kubernetes Resource Name Validator RFC 1123 subdomain, label, or 1035 Kubernetes Label & Annotation Validator Why your image tag is not a legal label Kubernetes CrashLoopBackOff Guide The status is a timer, not a cause Kubernetes TLS Certificate Decoder When it expires, and what it covers Kubernetes ServiceAccount Token Decoder Bound or legacy, and when it dies Kubernetes imagePullSecret Generator The registry URL nobody gets right Kubernetes cert-manager Certificates Issuer scope, and the rate limit that costs a week Kubernetes Flux HelmRelease Generator With the source it cannot work without Kubernetes Helm Chart Generator Chart.yaml, values, and working templates Kubernetes Argo CD Application Generator The defaults that are off, turned on Kubernetes Ingress Generator pathType, and the TLS secret that fails quietly Kubernetes SecurityContext Generator Pod level, container level, and which wins Kubernetes Probe Generator How long it gets to start before liveness kills it Kubernetes PVC Generator Access modes your storage can actually do Kubernetes ResourceQuota Generator And the LimitRange that stops it breaking deploys Kubernetes CronJob Generator The fields that decide whether it runs Kubernetes NetworkPolicy Generator With the DNS rule already in it Kubernetes RBAC Generator And what it actually grants Kubernetes PodDisruptionBudget Generator Can this budget ever be satisfied? Kubernetes API Deprecation Checker What breaks on the next upgrade Kubernetes Kubeconfig Viewer Read it without handing it to anyone Kubernetes Quantity Parser What 512Mi, 100m and 1e3 actually mean Kubernetes Multi-Document YAML Splitter One file per object, in apply order Kubernetes .env to ConfigMap & Secret Split the credentials out on the way Kubernetes Secret Decoder See what is actually in there Kubernetes Ingress to Gateway API Which annotations silently stop working Kubernetes Manifest Visualizer What points at what, and what points at nothing Kubernetes Patch Tester Which --type, and what it deletes Kubernetes kubectl JSONPath Tester kubectl's dialect, not generic JSONPath Kubernetes Node Affinity Tester Required excludes, preferred only scores Kubernetes Helm Values Diff A missing key is a third value Kubernetes Pod and Service CIDR Fixed at cluster creation Kubernetes Secret Encoder base64 is not encryption Kubernetes API Version Checker Will this apply on that cluster Kubernetes Go Template Tester Go truthiness is not JavaScript's Kubernetes Cluster Cost Estimator At your rates, not a price table Kubernetes Kustomize Build Transformers run in kustomize's order Kubernetes Helm Chart Validator version is SemVer, appVersion is not

    Elsewhere on the site