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
| Scenario | Use HPA | Use VPA | Use Both |
| Stateless web API | Yes | Recommendation mode | Yes (different axes) |
| Single-instance database | No | Yes (Auto mode) | No |
| Batch job workers | Yes | Recommendation mode | Possible |
| ML training pods | No | Yes | No |
| Event-driven functions | KEDA instead | Recommendation mode | Yes |
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
| Feature | HPA | VPA | KEDA |
| Scaling direction | Horizontal (replicas) | Vertical (resources) | Horizontal (replicas) |
| Scale to zero | No | No | Yes |
| Custom metrics | Yes (via adapter) | No | Yes (60+ built-in) |
| Requires restart | No | Yes (in Auto mode) | No |
| External event sources | No | No | Yes |
| Built into K8s | Yes | Addon | Addon |
| Best for | Stateless services | Right-sizing | Event-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
| Phase | Expected Behavior | Failure Indicator |
| Ramp up (50 VUs) | Stable at minReplicas | Pods already scaling at low load |
| Spike (500 VUs) | Scale up within 30-60s | P99 latency exceeds 1s before scaling |
| Hold at spike | Stable at scaled count | Constant scaling oscillation |
| Ramp down | Slow scale-down over 5min | Rapid pod termination |
| Post-test | Return to minReplicas | Pods 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.