Kubernetes Autoscaling Guide: HPA, VPA, and KEDA Compared

15 min read
kubernetesautoscalinghpavpakedak6load-testingprometheus

Configure HPA with custom Prometheus metrics, deploy VPA in recommendation mode, set up KEDA ScaledObjects for event-driven workloads, and validate everything with k6 load tests. This guide covers when to use each autoscaler, how they interact, and the exact YAML to deploy them.

Kubernetes offers three autoscaling mechanisms, each solving a different problem. The Horizontal Pod Autoscaler (HPA) adds or removes pod replicas. The Vertical Pod Autoscaler (VPA) adjusts CPU and memory requests on existing pods. KEDA extends HPA with event-driven scaling from external sources like message queues. According to the CNCF 2024 survey, 72% of organizations use HPA, but only 18% use VPA and 23% use KEDA, leaving significant optimization potential on the table.

---

1. HPA with Custom Prometheus Metrics

The built-in CPU and memory metrics are a starting point, but real workloads need custom metrics. An API server should scale on request latency or queue depth, not just CPU. HPA v2 supports custom metrics through the Prometheus adapter, giving you access to any metric your application exports.

Install the Prometheus adapter

<h1>Add the Prometheus community Helm repo</h1>

helm repo add prometheus-community https://prometheus-community.github.io/helm-charts

helm repo update

<h1>Install the adapter with custom metric rules</h1>

helm install prometheus-adapter prometheus-community/prometheus-adapter \

--namespace monitoring \

--set prometheus.url=http://prometheus-server.monitoring.svc \

--set prometheus.port=9090 \

-f adapter-values.yaml

adapter-values.yaml with custom rules

rules:

custom:

- seriesQuery: http_requests_per_second

resources:

overrides:

namespace:

resource: namespace

pod:

resource: pod

name:

matches: "^(.*)$"

as: "http_rps"

metricsQuery: sum(rate(http_requests_total{<<.LabelMatchers>>}[2m])) by (<<.GroupBy>>)

- seriesQuery: http_request_duration_seconds_bucket

resources:

overrides:

namespace:

resource: namespace

pod:

resource: pod

name:

matches: "^(.*)$"

as: "http_p99_latency"

metricsQuery: histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket{<<.LabelMatchers>>}[5m])) by (le, <<.GroupBy>>))

HPA using custom metrics

apiVersion: autoscaling/v2

kind: HorizontalPodAutoscaler

metadata:

name: api-server-hpa

namespace: production

spec:

scaleTargetRef:

apiVersion: apps/v1

kind: Deployment

name: api-server

minReplicas: 3

maxReplicas: 50

behavior:

scaleDown:

stabilizationWindowSeconds: 300

policies:

- type: Percent

value: 20

periodSeconds: 60

scaleUp:

stabilizationWindowSeconds: 0

policies:

- type: Percent

value: 100

periodSeconds: 15

- type: Pods

value: 5

periodSeconds: 15

selectPolicy: Max

metrics:

- type: Resource

resource:

name: cpu

target:

type: Utilization

averageUtilization: 65

- type: Pods

pods:

metric:

name: http_rps

target:

type: AverageValue

averageValue: "1000"

- type: Pods

pods:

metric:

name: http_p99_latency

target:

type: AverageValue

averageValue: "500m"

This HPA scales on three signals simultaneously:

  • CPU utilization at 65% - the safety net for compute-bound spikes
  • HTTP requests per second at 1000 - prevents individual pods from being overwhelmed
  • P99 latency at 500ms - scales up when latency degrades, ensuring SLO compliance

The HPA uses the highest recommendation across all three metrics (it takes the max), so any metric breaching its target triggers a scale-up. The scale-up `selectPolicy: Max` ensures aggressive scaling when needed.

---

2. VPA in Recommendation Mode

The Vertical Pod Autoscaler analyzes historical resource usage and recommends (or automatically applies) optimal CPU and memory requests. In production, use recommendation mode to get suggestions without automatic restarts. According to Google's 2025 GKE optimization report, VPA recommendations reduce resource waste by 25-35% compared to manually set requests.

Install VPA

<h1>Clone the VPA repository</h1>

git clone https://github.com/kubernetes/autoscaler.git

cd autoscaler/vertical-pod-autoscaler

<h1>Install VPA components</h1>

./hack/vpa-up.sh

<h1>Verify installation</h1>

kubectl get pods -n kube-system | grep vpa

VPA in recommendation mode

apiVersion: autoscaling.k8s.io/v1

kind: VerticalPodAutoscaler

metadata:

name: api-server-vpa

namespace: production

spec:

targetRef:

apiVersion: apps/v1

kind: Deployment

name: api-server

updatePolicy:

updateMode: "Off"

resourcePolicy:

containerPolicies:

- containerName: api

minAllowed:

cpu: 50m

memory: 64Mi

maxAllowed:

cpu: "4"

memory: 8Gi

controlledResources: ["cpu", "memory"]

Reading VPA recommendations

<h1>Get VPA recommendations for all deployments</h1>

kubectl get vpa -A -o jsonpath='{range .items[*]}

{.metadata.namespace}/{.metadata.name}:

Target CPU={.status.recommendation.containerRecommendations[0].target.cpu}

Target Mem={.status.recommendation.containerRecommendations[0].target.memory}

{end}'

VPA vs HPA decision matrix

ScenarioUse HPAUse VPAUse Both

Stateless web APIYesRecommendation modeYes (different axes)
Single-instance databaseNoYes (Auto mode)No
Batch job workersYesRecommendation modePossible
ML training podsNoYesNo
Event-driven functionsKEDA insteadRecommendation modeYes

Important: HPA and VPA cannot both control the same metric (CPU or memory) on the same deployment. If you use HPA for CPU scaling, VPA should only recommend memory adjustments, or run VPA in recommendation-only mode. Running both in auto mode on the same resource causes oscillation.

Teams using Arxentra's managed infrastructure get VPA recommendations surfaced directly in the dashboard without needing to install additional components.

---

3. KEDA for Event-Driven Scaling

KEDA (Kubernetes Event-Driven Autoscaling) extends HPA with 60+ scalers for external event sources. Instead of scaling on CPU, you scale on RabbitMQ queue depth, Kafka consumer lag, PostgreSQL query results, or Prometheus metrics. KEDA can also scale to zero, which HPA cannot. According to the 2024 KEDA adoption survey, KEDA reduces average pod count by 40-60% for event-driven workloads compared to static replica counts.

Install KEDA

helm repo add kedacore https://kedacore.github.io/charts

helm repo update

helm install keda kedacore/keda \

--namespace keda \

--create-namespace \

--set watchNamespace=""

ScaledObject for RabbitMQ queue processing

apiVersion: keda.sh/v1alpha1

kind: ScaledObject

metadata:

name: order-processor

namespace: production

spec:

scaleTargetRef:

name: order-processor

pollingInterval: 10

cooldownPeriod: 300

idleReplicaCount: 0

minReplicaCount: 0

maxReplicaCount: 30

triggers:

- type: rabbitmq

metadata:

protocol: amqp

queueName: orders

mode: QueueLength

value: "5"

authenticationRef:

name: rabbitmq-auth

---

apiVersion: keda.sh/v1alpha1

kind: TriggerAuthentication

metadata:

name: rabbitmq-auth

namespace: production

spec:

secretTargetRef:

- parameter: host

name: rabbitmq-credentials

key: connection-string

ScaledObject for Prometheus metrics (HTTP queue depth)

apiVersion: keda.sh/v1alpha1

kind: ScaledObject

metadata:

name: api-gateway-scaler

namespace: production

spec:

scaleTargetRef:

name: api-gateway

pollingInterval: 15

cooldownPeriod: 120

minReplicaCount: 2

maxReplicaCount: 100

triggers:

- type: prometheus

metadata:

serverAddress: http://prometheus-server.monitoring.svc:9090

metricName: http_requests_queued

query: sum(http_requests_queued{namespace="production"})

threshold: "100"

- type: prometheus

metadata:

serverAddress: http://prometheus-server.monitoring.svc:9090

metricName: http_p99_latency_ms

query: histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket{namespace="production"}[2m])) by (le)) * 1000

threshold: "500"

Autoscaler comparison table

FeatureHPAVPAKEDA

Scaling directionHorizontal (replicas)Vertical (resources)Horizontal (replicas)
Scale to zeroNoNoYes
Custom metricsYes (via adapter)NoYes (60+ built-in)
Requires restartNoYes (in Auto mode)No
External event sourcesNoNoYes
Built into K8sYesAddonAddon
Best forStateless servicesRight-sizingEvent-driven workloads

---

4. Validate Autoscaling with k6 Load Tests

Autoscaling configurations are only as good as their testing. k6 is an open-source load testing tool that generates realistic traffic patterns. Always load test your autoscaling setup before relying on it in production.

k6 load test script

// k6-autoscale-test.js

import http from "k6/http";

import { check, sleep } from "k6";

import { Rate } from "k6/metrics";

const errorRate = new Rate("errors");

export const options = {

stages: [

// Ramp up to baseline

{ duration: "2m", target: 50 },

// Hold at baseline (HPA should be at minReplicas)

{ duration: "3m", target: 50 },

// Spike to trigger scale-up

{ duration: "1m", target: 500 },

// Hold at spike (HPA should scale up)

{ duration: "5m", target: 500 },

// Ramp down (test scale-down stabilization)

{ duration: "2m", target: 50 },

// Hold at baseline (verify slow scale-down)

{ duration: "10m", target: 50 },

// Ramp to zero

{ duration: "1m", target: 0 },

],

thresholds: {

http_req_duration: ["p(99)<1000"],

errors: ["rate<0.01"],

},

};

export default function () {

const res = http.get("http://api-server.production.svc:8080/api/health");

check(res, {

"status is 200": (r) => r.status === 200,

"latency < 500ms": (r) => r.timings.duration < 500,

});

errorRate.add(res.status !== 200);

sleep(0.1);

}

Run the test and observe scaling

<h1>Run k6 from within the cluster (as a Job)</h1>

kubectl run k6-test --image=grafana/k6 --restart=Never -- run - < k6-autoscale-test.js

<h1>In another terminal, watch HPA scaling</h1>

kubectl get hpa api-server-hpa -w

<h1>Watch pod count changes</h1>

kubectl get pods -l app=api-server -w

<h1>Check VPA recommendations after the test</h1>

kubectl describe vpa api-server-vpa

What to verify during load testing

PhaseExpected BehaviorFailure Indicator

Ramp up (50 VUs)Stable at minReplicasPods already scaling at low load
Spike (500 VUs)Scale up within 30-60sP99 latency exceeds 1s before scaling
Hold at spikeStable at scaled countConstant scaling oscillation
Ramp downSlow scale-down over 5minRapid pod termination
Post-testReturn to minReplicasPods stuck at high count

For comprehensive autoscaling validation on Arxentra's platform, the dashboard shows real-time scaling events alongside latency metrics, making it easy to correlate scaling decisions with user experience.

---

Frequently Asked Questions

Can I use HPA and KEDA together?

Yes, but not on the same deployment. KEDA creates HPA objects under the hood, so applying both to the same deployment causes conflicts. Use KEDA for event-driven workloads and native HPA for resource-based scaling on separate deployments. If you need both event-driven and resource-based scaling on one deployment, use KEDA with a Prometheus trigger that exposes CPU as a custom metric.

Should I use VPA Auto mode in production?

For most stateless workloads, no. VPA Auto mode restarts pods to apply new resource values, which can cause brief availability dips. Use recommendation mode (updateMode: "Off") and apply changes during maintenance windows. The exception is single-instance stateful workloads like databases where horizontal scaling is not possible. Read our Kubernetes cost control checklist for more context on resource right-sizing.

What is the minimum metrics history VPA needs?

VPA needs at least 24 hours of metrics data to make reliable recommendations. For workloads with weekly traffic patterns (like B2B SaaS), wait at least 7 days before trusting VPA recommendations. The VPA updater checks metrics every 1 minute and recalculates recommendations every 8 hours by default.

How does KEDA scale to zero work?

When the KEDA scaler reports zero events (empty queue, no pending messages), KEDA scales the deployment to `idleReplicaCount` (default 0). The first incoming event triggers a scale from zero to `minReplicaCount`. Cold start time depends on your container image size and initialization time. For latency-sensitive workloads, set `minReplicaCount: 1` instead of scaling to zero.

How do I choose between HPA custom metrics and KEDA?

Use native HPA custom metrics when you are already running Prometheus and need to scale on application-level metrics (RPS, latency, error rate). Use KEDA when you need to scale on external event sources (message queues, databases, cloud services) or when you need scale-to-zero capability. KEDA is also easier to configure for non-Prometheus metric sources because it has built-in scalers.

What happens if my metrics pipeline (Prometheus) goes down?

If the Prometheus adapter cannot reach Prometheus, HPA falls back to the last known metric values and stops scaling. It does not scale to zero or max. Set up alerting on Prometheus availability and consider running Prometheus in HA mode (two replicas with Thanos or Victoria Metrics for deduplication). Check our observability stack guide for production-grade monitoring setup.