KEDA: Event-Driven Autoscaling & Scale-to-Zero on K8s

Photo by Samuel Wolfl on Pexels
KEDA (Kubernetes Event-Driven Autoscaling) is a CNCF graduated add-on that scales workloads based on external event sources such as queue depth, Kafka lag, Prometheus queries, or a cron schedule instead of just CPU or memory. It works alongside the Horizontal Pod Autoscaler rather than replacing it.
KEDA does not replace the HPA; it drives one. The KEDA operator handles activation edges (zero-to-one and one-to-zero), while a standard HPA that KEDA creates handles one-to-N scaling. A metrics API server exposes the event-source numbers as Kubernetes external metrics that the HPA reads like it reads CPU.
Yes. Setting minReplicaCount to 0 (its default) on a ScaledObject enables true scale-to-zero. When the event source has been idle for the cooldownPeriod, KEDA scales the workload down to zero pods, so idle workers cost nothing until the next event arrives.
When a deployment sits at zero, the first event must wait out KEDA's polling interval, then the pod is scheduled, the image pulled if uncached, the container started, and the readiness probe satisfied. That adds seconds to tens of seconds of latency, so you should not scale latency-sensitive, user-facing services to zero.
Yes. KEDA started as a Microsoft and Red Hat collaboration in 2019, entered the CNCF Sandbox in March 2020, moved to the Incubator in August 2021, and graduated as a CNCF project on 22 August 2023. It runs in production at organizations including FedEx, Grafana Labs, Reddit, and Xbox.

Photo by Samuel Wolfl on Pexels
Key Takeaway
KEDA (Kubernetes Event-Driven Autoscaling) is a CNCF graduated add-on that scales workloads on external signals such as queue depth, Kafka lag, Prometheus queries, or a cron schedule instead of CPU. It wraps the Horizontal Pod Autoscaler through a ScaledObject and can scale idle deployments all the way to zero replicas to cut cost.
Most Kubernetes autoscaling advice stops at the Horizontal Pod Autoscaler watching CPU. That works for a web service whose CPU rises with traffic, but it falls apart for the event-driven half of my systems: a worker draining a RabbitMQ queue, a consumer chasing Kafka lag, a nightly batch job. Those pods can be pinned at high CPU while the backlog is empty, or idle at low CPU while thousands of messages pile up. CPU is simply the wrong signal.
KEDA fixes that by scaling on the event source itself. It began as a Microsoft and Red Hat collaboration in 2019, entered the CNCF Sandbox in March 2020, moved to the Incubator in August 2021, and graduated as a CNCF project on 22 August 2023 — the same maturity tier as Kubernetes itself. It runs in production at organisations including FedEx, Grafana Labs, Reddit, and Xbox, so it is not a science project.
The HPA answers one question: are my pods busy? For a request-serving API that is a decent proxy for load, because more requests mean more CPU. But a queue consumer decouples arrival from processing. Ten thousand messages can arrive in a burst while the single running pod chews through them at a steady, unremarkable CPU. The HPA sees nothing alarming and never scales out, so your backlog and your latency both climb while the dashboard looks calm.
What you actually care about is the backlog, not the CPU. The right scaling signal is the queue depth, the Kafka consumer lag, a Prometheus metric, or even the clock. KEDA lets you scale directly on those numbers, and it can hold the deployment at zero replicas when there is no work at all — something the plain HPA cannot do, because its floor is one.
KEDA does not reimplement autoscaling; it extends what Kubernetes already has. You install it as an operator, and it splits the job in two. The KEDA operator handles the activation edges — bringing a deployment from zero to one when the first event arrives, and back from one to zero when the source goes quiet. Everything above one replica is delegated to a standard HPA that KEDA creates and manages for you.
The bridge is the KEDA metrics API server, which exposes the numbers polled from your event source (Kafka, RabbitMQ, SQS, Prometheus, and more) as Kubernetes external metrics. The generated HPA reads those metrics exactly as it would read CPU, and scales one-to-N against your threshold. You describe all of this in a single custom resource, the ScaledObject, and KEDA keeps the underlying HPA in sync with it.
Because KEDA drives a real HPA, everything you already know about HPA behaviour still applies. You can tune stabilisation windows and scale-up or scale-down rates through the ScaledObject advanced section, and any custom scaling behaviour you set flows straight into the generated HPA — no separate HPA object to maintain.
A ScaledObject points at the workload to scale and lists one or more triggers. The two fields that matter most for cost are minReplicaCount, which defaults to zero and is what enables scale-to-zero, and maxReplicaCount, which caps the HPA at 100 by default. KEDA checks each trigger on the pollingInterval (30 seconds by default) and, once a source has been idle for the cooldownPeriod (300 seconds by default), scales the workload back down to zero.
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: orders-consumer
namespace: default
spec:
scaleTargetRef:
name: orders-consumer # the Deployment to scale
pollingInterval: 30 # check the trigger every 30s (default)
cooldownPeriod: 300 # wait 300s idle before scaling to 0 (default)
minReplicaCount: 0 # true scale-to-zero
maxReplicaCount: 20
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus.monitoring.svc:9090
query: sum(rabbitmq_queue_messages_ready{queue="orders"})
threshold: "50" # target ~50 messages per replica
activationThreshold: "5" # only wake from 0 once 5+ messages queueThe threshold field is the target value per replica: with a threshold of 50 and 500 messages waiting, the HPA aims for roughly ten replicas. The separate activationThreshold controls the zero-to-one edge only — it stops KEDA waking the whole deployment for a single stray message, which matters when cold starts are expensive. Set it above zero for any workload where waking up is not free.
A scaler is the connector that reads one kind of event source. KEDA shipped with more than 60 built-in scalers at graduation and has kept adding them, so most of the common sources are covered out of the box. The ones I reach for most:
Triggers compose. You can put a cron trigger and a Prometheus trigger on the same ScaledObject; KEDA takes the highest replica count any active trigger asks for. That combination is how I pre-warm a service to a baseline before the morning rush with cron, while still letting the live metric scale it further if real demand overshoots the forecast.
Scale-to-zero is free money only when idle time is cheap and latency is not. When a deployment sits at zero, the first event does not get an instant response — it waits out KEDA's polling interval to be noticed, then the pod has to be scheduled, the image pulled if it is not cached, the container started, and the readiness probe satisfied. For a heavy runtime that can be seconds to tens of seconds of added latency on the first request after idle.
Never scale a latency-sensitive, user-facing service to zero. The cold-start delay lands on a real user's request. Keep minReplicaCount at one or more for anything on the interactive path, and reserve scale-to-zero for background workers, batch consumers, and internal jobs where a few seconds of wake-up time is invisible.
| Concern | HPA on CPU or memory | KEDA (event-driven) |
|---|---|---|
| Scales on | CPU or memory utilisation | Queue depth, Kafka lag, PromQL, cron, and 60-plus sources |
| Minimum replicas | One — cannot reach zero | Zero — true scale-to-zero |
| Reaction to backlog | Indirect, only after CPU rises | Direct, on the backlog metric itself |
| Idle cost | Pays for at least one pod | No pods running when idle |
| How it runs | Built into Kubernetes | Add-on operator plus a ScaledObject that drives an HPA |
| Cold start | None — always warm | First event after idle waits for pod startup |
My rule of thumb: if the work is user-facing and latency-bound, let the HPA scale it on CPU and keep a warm floor. If the work is event-driven and bursty — queue consumers, Kafka pipelines, scheduled batches — hand it to KEDA, pick the scaler that matches the real signal, and let it fall to zero between bursts. Start with one non-critical worker, wire a single trigger, and watch it wake and sleep before you trust it with anything busier.