KEDA
KEDA is an event-based workload autoscaler that works alongside the standard HorizontalPodAutoscaler, and it can scale all the way down to zero.
KEDA does not replace the HPA. When you create a ScaledObject, KEDA creates and owns a HorizontalPodAutoscaler named keda-hpa-<name> in your namespace and feeds it the metric it computes for you.
Note: If CPU or memory is all you need use a plain HPA, see HorizontalPodAutoscaler.
What you can create
Two resources are available in your namespace:
| Resource | What it does |
|---|---|
ScaledObject | Scales a long-running workload — a Deployment or a StatefulSet |
ScaledJob | Creates Jobs, one per unit of work, instead of scaling a Deployment |
KEDA's other resource types (TriggerAuthentication, ClusterTriggerAuthentication, CloudEventSource) aren't exposed yet, so triggers needing their own credentials (SQS, Kafka, databases, etc.) are not yet available. We may add this based on demand. Note that the Auth related resources typically require giving KEDA read access to all secrets in the cluster, which carries a security cost.
ScaledObject
Example of scaling a Deployment on a Prometheus query giving the request rate to the backend, between 2 and 10 replicas:
---
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: $CI_ENVIRONMENT_SLUG
namespace: $KUBE_NAMESPACE
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: $CI_ENVIRONMENT_SLUG
minReplicaCount: 2
maxReplicaCount: 10
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus-system.monitoring.svc:9090
query: sum(rate(haproxy_backend_sessions_total{proxy="${KUBE_NAMESPACE}_svc_${CI_ENVIRONMENT_SLUG}_web"}[1m]))
threshold: "500"
The HAProxy ingress controller exports one backend per Service an Ingress routes to, and the proxy label carries that backend's name. Confirm the exact value for yours in Grafana before you build a ScaledObject around it.
The query uses rate() over a counter (haproxy_backend_sessions_total) rather than a gauge like haproxy_backend_current_sessions. Session/connection counts are snapshotted at scrape time, so short-lived, fast-completing requests can pass through a backend without ever being caught by a scrape — the gauge can sit at zero under real, sustained load. A rate over a counter avoids that: it reflects requests-per-second over the whole time window, not an instantaneous sample, which makes it a much more reliable trigger for typical HTTP backends. Use a gauge like current_sessions instead only when connections are genuinely long-lived relative to your scrape interval (e.g. WebSockets, streaming, long-polling), so that concurrency is a real, sustained number rather than a fleeting instant.
threshold is per replica. KEDA divides the query result by it to get the replica count it wants: at 500 requests/sec the example above asks for 1 replica, at 5000 requests/sec it asks for 10, and minReplicaCount/maxReplicaCount clamp the result.
Some example output from the hello-keda example project. Note that the threshold and replica count in this project are set artificially
low for demonstration purposes.
$ kubectl -n platon-paas2-examples-hello-keda get scaledobject
NAME SCALETARGETKIND SCALETARGETNAME MIN MAX READY ACTIVE FALLBACK PAUSED TRIGGERS AUTHENTICATIONS AGE
production apps/v1.Deployment production 1 2 True True False False prometheus 2m21s
ACTIVE is whether the trigger currently reports load, and FALLBACK turns True when KEDA cannot reach the trigger and is serving spec.fallback instead.
Once the ScaledObject exists, the HPA KEDA created for it shows the value it is actually getting — external metrics are reported in milli-units:
$ kubectl -n platon-paas2-examples-hello-keda get hpa
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
keda-hpa-production Deployment/production 33m/500 (avg) 1 2 1 3m5s
Use describe on the hpa and scaledobject to see detailed information.
$ kubectl -n platon-paas2-examples-hello-keda describe hpa
Name: keda-hpa-production
Namespace: platon-paas2-examples-hello-keda
Labels: app.kubernetes.io/managed-by=keda-operator
app.kubernetes.io/name=keda-hpa-production
app.kubernetes.io/part-of=production
app.kubernetes.io/version=2.20.2
scaledobject.keda.sh/name=production
Annotations: <none>
CreationTimestamp: Wed, 23 Sep 2026 08:34:24 +0200
Reference: Deployment/production
Metrics: ( current / target )
"s0-prometheus" (target average value): 1760m / 500
Min replicas: 1
Max replicas: 2
Deployment pods: 1 current / 1 desired
Conditions:
Type Status Reason Message
---- ------ ------ -------
AbleToScale True ReadyForNewScale recommended size matches current size
ScalingActive True ValidMetricFound the HPA was able to successfully calculate a replica count from external metric s0-prometheus(&LabelSelector{MatchLabels:map[string]string{scaledobject.keda.sh/name: production,},MatchExpressions:[]LabelSelectorRequirement{},})
ScalingLimited False DesiredWithinRange the desired count is within the acceptable range
Events: <none>
$ kubectl -n platon-paas2-examples-hello-keda describe scaledobject
Name: production
Namespace: platon-paas2-examples-hello-keda
Labels: scaledobject.keda.sh/name=production
Annotations: <none>
API Version: keda.sh/v1alpha1
Kind: ScaledObject
Metadata:
Creation Timestamp: 2026-09-23T06:34:24Z
Finalizers:
finalizer.keda.sh
Generation: 1
Resource Version: 1188061099
UID: 08ae108b-4ef4-4056-8d51-a7e3da619132
Spec:
Max Replica Count: 2
Min Replica Count: 1
Scale Target Ref:
API Version: apps/v1
Kind: Deployment
Name: production
Triggers:
Metadata:
Query: sum(rate(haproxy_backend_sessions_total{proxy="platon-paas2-examples-hello-keda_svc_production_web"}[1m]))
Server Address: http://prometheus-system.monitoring.svc:9090
Threshold: 500
Type: prometheus
Status:
Authentications Types:
Conditions:
Message: ScaledObject is defined correctly and is ready for scaling
Reason: ScaledObjectReady
Status: True
Type: Ready
Message: Scaling is performed because triggers are active
Reason: ScalerActive
Status: True
Type: Active
Message: No fallbacks are active on this scaled object
Reason: NoFallbackFound
Status: False
Type: Fallback
Status: False
Type: Paused
Message: HPA is actively scaling
Reason: HPAActive
Status: True
Type: HPAActive
External Metric Names:
s0-prometheus
Hpa Name: keda-hpa-production
Last Active Time: 2026-09-23T06:39:54Z
Original Replica Count: 1
Scale Target GVKR:
Group: apps
Kind: Deployment
Resource: deployments
Version: v1
Scale Target Kind: apps/v1.Deployment
Triggers Activity:
s0-prometheus:
Is Active: true
Triggers Types: prometheus
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal KEDAScalersStarted 5m43s keda-operator Scaler prometheus is built
Normal KEDAScalersStarted 5m43s keda-operator Started scalers watch
Normal ScaledObjectReady 5m43s keda-operator ScaledObject is ready for scaling
Scaling to zero
minReplicaCount: 0 lets a workload go away entirely when nothing is happening. A cron trigger is the simplest way to do it. Here the workload runs 3 replicas on weekdays during office hours and nothing outside them:
spec:
scaleTargetRef:
name: batch-api
minReplicaCount: 0
maxReplicaCount: 5
triggers:
- type: cron
metadata:
timezone: Europe/Oslo
start: 0 7 * * 1-5
end: 0 18 * * 1-5
desiredReplicas: "3"
KEDA does have a HTTP Add-on which puts an interceptor in front of the Service to hold the request while the workload starts. It is in Beta and currently not deployed to PaaS.
ScaledJob
Use a ScaledJob when each unit of work should be its own Job. KEDA creates up to maxReplicaCount Jobs based on how much work the trigger reports.
---
apiVersion: keda.sh/v1alpha1
kind: ScaledJob
metadata:
name: batch-worker
namespace: app-namespace
spec:
jobTargetRef:
backoffLimit: 3
template:
spec:
restartPolicy: Never
containers:
- name: worker
image: worker:1.0.0
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
memory: 256Mi
pollingInterval: 30
maxReplicaCount: 10
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 2
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus-system.monitoring.svc:9090
query: sum(myapp_queue_depth{namespace="app-namespace"})
threshold: "5"
A Job that KEDA has started runs to completion, scaling down doesn't kill the job pods.
Triggers you can use
These need no credentials and work today:
prometheus— any query against the in-cluster Prometheus.cron— a schedule, as above.cpuandmemory— the same signals a plain HPA uses.ScaledObjectonly, notScaledJob— these triggers scale via the Kubernetes Metrics Server directly through the HPA, andScaledJobdoesn't use an HPA. They also can't scale to zero on their own.kubernetes-workload— the number of pods matching a label selector.
The prometheus trigger queries the cluster's own Prometheus, which is not the same store as the Mimir data source you use in grafana.platon.sikt.no. Your metric has to be scraped in the cluster through a ServiceMonitor or PodMonitor for KEDA to see it. See Prometheus Rules Alerting example project for more info. Metrics your application ships over OTLP go straight to Mimir and are not queryable here — see Metrics.
Prometheus is not reachable from your namespace — a NetworkPolicy in monitoring admits only the KEDA operator and a few system components. To check a query before you build a ScaledObject around it use Platon Grafana, select your tenant and go to Explore with Platon App Clusters as source.
Limitations & considerations
- One autoscaler per workload. Creating a
ScaledObjectfor a workload that already has its own HPA is rejected — delete the HPA first. KEDA's ownkeda-hpa-*HPA is managed for you, so do not edit it; your changes get reconciled away. cpuandmemorytriggers cannot scale to zero. AScaledObjectthat only uses them needsminReplicaCount: 1or higher. Combine with acronorprometheustrigger to get to zero.cpuandmemorytriggers need requests set on the pods, exactly as a plain HPA does — see Setting resources.- Only Deployments and StatefulSets can be scaled. Custom workload kinds are not enabled.
- Scaling is not instant. KEDA re-evaluates triggers every
pollingInterval(30 seconds by default). Scaling down is then the HPA's decision, and it waits out a stabilization window (300 seconds by default) before it reduces replicas — tune that withspec.advanced.horizontalPodAutoscalerConfig.behavior.scaleDown.stabilizationWindowSeconds.cooldownPeriod(300 seconds by default) is a separate setting that governs only the last step, from one replica to zero. - Deleting the
ScaledObjectleaves the workload where it is — at whatever replica count it was last scaled to, not the one it had before KEDA took over. Setspec.advanced.restoreToOriginalReplicaCount: trueon theScaledObjectif you want the original count put back when it is deleted. - With several triggers, the largest wins. KEDA scales to the highest replica count any single trigger asks for.