Set Up Kubernetes HPA Autoscaling: 12 Steps, 60 Min [2026] A tutorial published in October 2026 walks through a 12-step, 60-minute Kubernetes Horizontal Pod Autoscaler (HPA) setup covering CPU and memory scaling, custom Prometheus metrics, scale-to-zero, and cost reporting, targeting clusters running Kubernetes 1.30 or newer. The guide cites Google Cloud's AI infrastructure update describing GKE Agent Substrate, which Google says runs millions of sandboxes at roughly 10x the density of a standard container runtime with sub-500-millisecond resume and more than 500 suspend-and-resume activations per second in internal testing, plus GKE's native scale-to-zero built on KEP-2021 and Pod snapshots that Google reports cut AI-inference cold-start time by as much as 89%. The tutorial notes Kubernetes was used by 28.5% of respondents in the 2025 Stack Overflow Developer Survey and argues that spiky AI-agent and inference workloads make fixed replica counts untenable. A Kubernetes deployment that cannot scale itself is just a slower, more expensive way to run a fixed number of pods. The Horizontal Pod Autoscaler HPA is the control loop that fixes that, watching CPU, memory, or any custom signal you choose, then adding or removing pod replicas to match demand. It ships inside every Kubernetes distribution, it costs nothing extra to turn on, and as of October 2026 it has absorbed a wave of new tricks, from scale-to-zero on managed clusters to the custom Prometheus metrics pipelines that power today’s AI-agent workloads. This tutorial builds a working Kubernetes HPA setup from a blank cluster to a production-ready configuration that scales on CPU, scales on custom application metrics, scales to zero when idle, and reports what it costs, one of the core cloud computing https://shattered.io/cloud-computing/ skills worth having regardless of which provider’s Kubernetes offering you run it on. By the end you will have a complete, tested project you can drop into any cluster running Kubernetes 1.30 or newer, including the Kubernetes 1.37 release now rolling out through Google Kubernetes Engine’s Rapid channel. Don't miss new tech stories on Google Add Shattered once in the Google app and our stories show up in your news suggestions. Add now https://www.google.com/preferences/source?hl=en&q=https%3A%2F%2Fshattered.io%2F Why Kubernetes HPA autoscaling matters more in 2026 Kubernetes adoption has not slowed down. Kubernetes was used by 28.5% of all respondents https://survey.stackoverflow.co/2025/technology in the 2025 Stack Overflow Developer Survey, cementing its place as the default way teams run containers at scale. What has changed is what those containers are doing. A growing share of workloads are AI agents and inference services with spiky, unpredictable load, and the old approach of pinning a fixed replica count no longer holds up financially or operationally. Google’s engineering teams have been pushing autoscaling hardest on this front. Google Cloud https://shattered.io/google-cloud-u4-ultra-low-latency-trading-2026/ ‘s AI infrastructure update https://cloud.google.com/blog/topics/ai-infrastructure/whats-new-in-ai-infrastructure-this-month describes GKE Agent Substrate, a runtime built for high-density agent workloads that the company says can run millions of sandboxes with roughly 10x the density of a standard container runtime, sub-500-millisecond resume operations, and more than 500 suspend-and-resume activations per second in Google’s internal testing. The same update covers GKE’s native scale-to-zero capability, which pairs the standard Horizontal Pod Autoscaler with a Kubernetes Enhancement Proposal KEP-2021 metric adapter so idle services drop to zero replicas instead of idling at one. Google also reports that Pod snapshots, which preserve CPU and GPU memory state for later restoration, cut AI-inference cold-start time by as much as 89% in its internal tests. None of that changes the fundamentals of how HPA works. It changes what you point it at. This tutorial covers the standard CPU and memory path first, then layers on the custom-metrics and scale-to-zero techniques that make 2026-era autoscaling genuinely different from the HPA most engineers learned five years ago. If you are running agent-style workloads behind an API gateway, pair this guide with our walkthrough on confidential containers for AI on Kubernetes https://shattered.io/confidential-containers-ai-kubernetes-fortanix-2026/ , which covers the isolation side of the same problem. The reason this is worth a dedicated tutorial rather than a quick reference card is that most teams configure HPA once, during the initial rollout of a service, and never revisit it. That is fine for a stateless web tier with predictable diurnal traffic. It is not fine for an agent pool fielding bursty, unscheduled requests from other services, where the gap between “scaled correctly” and “either Pending pods or a six-figure idle GPU bill” can be a single misjudged target percentage. The steps below are ordered so that each one builds a piece you will actually reuse: a baseline CPU autoscaler, a custom-metrics path for anything CPU cannot represent, a scale-to-zero layer for genuinely idle periods, and the guardrails that keep all three from becoming a cost or security problem on their own. Prerequisites: tools and versions you need You do not need a production cloud account to follow this tutorial. A local cluster works for every step except the managed scale-to-zero section, which is called out separately. Install the following before you start: - A Kubernetes cluster on version 1.30 or newer. This guide is written against Kubernetes 1.37, now available in GKE’s Rapid channel, though every command here also runs on the 1.36.x line that most managed providers ship as their default or stable channel today. Use kind, minikube, k3d, GKE, EKS, or AKS, whichever you have access to. - kubectl matching your cluster’s minor version, within one version skew in either direction. - Helm 3.15 or newer , used to install the Prometheus Adapter and KEDA later in the tutorial. - metrics-server 0.7.x , the component that feeds CPU and memory data to the autoscaling/v2 API. - KEDA 2.16 or newer , used for the scale-to-zero and event-driven scaling steps. - A container registry and image build tool such as Docker or Podman, for the sample workload. - hey or k6 , a lightweight HTTP load-testing tool, to generate the traffic that triggers scaling. If you plan to run this against a managed provider rather than a local cluster, decide that now. Our comparison of ECS, EKS, and Fargate https://shattered.io/ecs-vs-eks-vs-fargate-2026/ is a useful reference if you have not picked a platform yet, since the autoscaling building blocks in this tutorial behave slightly differently depending on whether the control plane is self-managed, EKS, GKE, or AKS. A quick note on why each tool is on this list rather than left optional. metrics-server is non-negotiable, autoscaling/v2 cannot function without a resource metrics source. Helm is technically avoidable if you prefer raw manifests, but both the Prometheus Adapter and KEDA ship their supported install path as a Helm chart, and hand-rolling the equivalent RBAC and API service registrations is a lot of YAML to maintain for no real benefit. KEDA specifically matters even if you never touch its event-source scalers, because it is the most widely adopted way to get scale-to-zero behavior on a cluster that is not GKE, and its ScaledObject resource is intentionally designed to generate and manage a standard HPA object rather than replace one, so everything you learn about reading HPA status later in this guide still applies once KEDA is in the picture. Step 1 and 2: provision a cluster and deploy the metrics server Step 1: Create or connect to your cluster If you are using kind locally, create a three-node cluster so the autoscaler has somewhere to put new pods: kind create cluster --name hpa-tutorial --config - < On a managed provider, swap this for your normal cluster-creation command, for example gcloud container clusters create or eksctl create cluster . Confirm your server version before continuing: kubectl version --short Client Version: v1.37.x Server Version: v1.36.4 or newer Step 2: Deploy the metrics server HPA's default autoscaling/v2 API needs a resource metrics source. metrics-server is the standard choice. Most managed clusters ship it already; local clusters almost never do. kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml kind clusters need kubelet TLS verification disabled for metrics-server kubectl patch deployment metrics-server -n kube-system --type=json \ -p ' {"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"} ' kubectl rollout status deployment metrics-server -n kube-system kubectl top nodes If kubectl top nodes returns real numbers instead of an error, metrics-server is wired up correctly and HPA has something to read. Step 3 and 4: deploy a sample workload and write your first HPA manifest Step 3: Deploy a CPU-bound sample service Use a small PHP-Apache image that burns CPU on request, a standard pattern for demonstrating autoscaling because it is cheap to load-test and easy to reason about. kubectl create deployment php-apache --image=registry.k8s.io/hpa-example --port=80 kubectl set resources deployment php-apache --requests=cpu=200m,memory=128Mi --limits=cpu=500m,memory=256Mi kubectl expose deployment php-apache --port=80 --target-port=80 kubectl get deployment php-apache kubectl get svc php-apache Setting a CPU request is not optional. HPA's CPU utilization target is a percentage of the request, so a pod with no CPU request has no baseline to scale against and the autoscaler will refuse to compute a target. Step 4: Write the HorizontalPodAutoscaler manifest Save this as hpa-cpu.yaml . It targets 50% average CPU utilization across replicas, with a floor of two pods and a ceiling of ten. apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: php-apache-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: php-apache minReplicas: 2 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 50 The autoscaling/v2 API is the one to use in every new manifest. The older autoscaling/v1 only supports CPU utilization and has been functionally frozen for years, it still exists for backward compatibility but has no reason to appear in new YAML in 2026. Step 5 and 6: apply the HPA and load-test it Step 5: Apply and verify kubectl apply -f hpa-cpu.yaml kubectl get hpa php-apache-hpa --watch You should see output similar to this once metrics-server has reported a data point, usually within 30 to 60 seconds: NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE php-apache-hpa Deployment/php-apache 0%/50% 2 10 2 48s Step 6: Generate load and watch it scale Open a second terminal and hammer the service with a busy-loop pod, or use a dedicated load tool if you have one installed: kubectl run load-generator --image=busybox:1.36 --restart=Never -- /bin/sh -c \ "while true; do wget -q -O- http://php-apache; done" Within two to three polling intervals HPA checks every 15 seconds by default , the TARGETS column should climb well past 50%, and REPLICAS should start increasing: NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE php-apache-hpa Deployment/php-apache 305%/50% 2 10 2 3m02s php-apache-hpa Deployment/php-apache 287%/50% 2 10 4 3m32s php-apache-hpa Deployment/php-apache 156%/50% 2 10 6 4m02s php-apache-hpa Deployment/php-apache 48%/50% 2 10 6 5m17s Kill the load generator with kubectl delete pod load-generator and the replica count will drift back down to the minimum over the next several minutes, deliberately slower than the scale-up, which is covered in the stabilization window section below. Step 7: move beyond CPU with custom Prometheus metrics CPU utilization is a poor proxy for load on anything that is I/O-bound, queue-bound, or GPU-bound, which describes most inference and agent workloads. Google's own guidance on GKE autoscaling now points teams toward Horizontal Pod Autoscaler configurations driven by custom Prometheus-compatible PromQL metrics rather than CPU alone, letting scaling rules reflect application-specific conditions like queue depth or requests-per-second instead of a generic resource proxy. To wire a custom metric into HPA, you need an adapter that implements the Kubernetes custom metrics API and translates it from whatever you are actually monitoring, usually Prometheus. Install the Prometheus Adapter with Helm: helm repo add prometheus-community https://prometheus-community.github.io/helm-charts helm repo update helm install prometheus-adapter prometheus-community/prometheus-adapter \ --namespace monitoring --create-namespace \ --set prometheus.url=http://prometheus-server.monitoring.svc \ --set prometheus.port=80 Then define a rule that exposes your chosen PromQL query, for example http requests per second , as a custom metric, and reference it from a second HPA: apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: php-apache-hpa-custom spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: php-apache minReplicas: 2 maxReplicas: 20 metrics: - type: Pods pods: metric: name: http requests per second target: type: AverageValue averageValue: "100" Run kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1" to confirm the adapter is registered before you apply this manifest. If that call 404s, the HPA controller will silently fail to compute a target and the replica count will never move, which is one of the most common failure modes covered later in this guide. Step 8: add scale-to-zero with KEDA Standard HPA has a hard floor of one replica, it cannot scale a deployment to zero on its own. KEDA Kubernetes Event-Driven Autoscaling solves that by acting as a metrics source that can report zero when there is no work, then handing control back to a standard HPA object once at least one replica exists. This is the same pattern Google describes for GKE's native scale-to-zero, where the platform pairs HPA with a KEP-2021-compatible metric adapter so idle services drop to zero pods instead of idling at a minimum of one. helm repo add kedacore https://kedacore.github.io/charts helm repo update helm install keda kedacore/keda --namespace keda --create-namespace Define a ScaledObject that targets the same deployment and scales on a queue length, with minReplicaCount: 0 enabling the scale-to-zero behavior: apiVersion: keda.sh/v1alpha1 kind: ScaledObject metadata: name: php-apache-scaled spec: scaleTargetRef: name: php-apache minReplicaCount: 0 maxReplicaCount: 10 cooldownPeriod: 120 triggers: - type: prometheus metadata: serverAddress: http://prometheus-server.monitoring.svc metricName: http requests per second query: sum rate http requests total{app="php-apache"} 2m threshold: "50" KEDA creates and manages its own HPA object behind the scenes once the ScaledObject is applied. You can confirm this with kubectl get hpa , you will see a new HPA named after the ScaledObject with keda-hpa- as a prefix, fully compatible with everything you already learned about reading HPA status in Step 5. Step 9: tune scaling behavior with stabilization windows and policies The default HPA behavior scales up fast and scales down slowly, on purpose, to avoid flapping. The behavior field lets you override both directions explicitly. This is the section most tutorials skip and most production incidents trace back to. apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: php-apache-hpa-tuned spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: php-apache minReplicas: 2 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 50 behavior: scaleUp: stabilizationWindowSeconds: 0 policies: - type: Pods value: 4 periodSeconds: 60 scaleDown: stabilizationWindowSeconds: 300 policies: - type: Percent value: 25 periodSeconds: 60 This configuration adds up to four pods per minute when scaling up with no stabilization delay, and removes at most 25% of current replicas per minute after a five-minute stabilization window on the way down. The stabilization window works by looking back over the window and picking the largest recommended replica count seen in that period, which is what prevents a brief dip in traffic from immediately shedding pods you will need again in ninety seconds. Step 10: pair HPA with cluster and node autoscaling HPA changes how many pods exist. It has no opinion on whether your nodes have room for them. If HPA tries to schedule six more replicas onto a cluster with no spare capacity, those pods sit in Pending state indefinitely. Pair HPA with the Cluster Autoscaler, or your provider's equivalent such as GKE's node auto-provisioning or Karpenter on EKS, so new nodes appear when HPA needs somewhere to put pods. Check for pods stuck Pending due to insufficient node capacity kubectl get pods -o wide | grep Pending kubectl describe pod | grep -A5 Events If you see FailedScheduling events citing insufficient CPU or memory, that is the cluster autoscaler's job to fix, not HPA's. The two systems are independent control loops that happen to cooperate: HPA decides replica count, the node autoscaler decides node count, and a Pod Disruption Budget covered next decides how much churn either one is allowed to cause at once. Step 11: secure, monitor, and control costs for autoscaled workloads An autoscaler that nobody is watching is a cost incident waiting to happen. Three things belong in any production HPA rollout: a Pod Disruption Budget, image and policy scanning on whatever gets autoscaled, and a budget alert tied to node count. apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: php-apache-pdb spec: minAvailable: 1 selector: matchLabels: app: php-apache If your autoscaled workloads run arbitrary or third-party code, run them through a policy engine before they ever reach HPA's scaling path. Our guide to Kyverno pod security policies on Kubernetes https://shattered.io/kyverno-kubernetes-pod-security-2026/ covers admission-time enforcement that blocks misconfigured pods from being created in the first place, which matters more once autoscaling means dozens of replicas can be spun up from a single bad template in minutes. Pair that with routine image scanning, our roundup of free and paid container vulnerability scanners https://shattered.io/container-vulnerability-scanners-free-vs-paid-2026/ breaks down what each tool actually catches before those images get autoscaled across a cluster. On the cost side, every extra replica is extra node capacity, extra network egress, and on GPU-backed inference pods, meaningfully more spend per hour than a typical web pod. If you are autoscaling GPU workloads specifically, read our breakdown of Google Cloud's FinOps tooling for flexible GPU commitments https://shattered.io/google-cloud-finops-flexible-gpu-commitments-focus-1-4-2026/ before you raise maxReplicas past single digits, since an autoscaler with no ceiling and no budget alert will happily scale your bill along with your pods. Step 12: validate the complete pipeline end to end With every piece in place, run a final end-to-end check before calling the setup production-ready: kubectl get hpa kubectl get scaledobject kubectl get pdb kubectl top pods Confirm the autoscaling API group is healthy kubectl get --raw "/apis/autoscaling/v2" | head -5 kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1" | head -5 You should see every HPA object reporting live TARGETS numbers not