Core Concepts · TL;DR
3 min readTL;DR
Kubernetes' built-in Horizontal Pod Autoscaler (HPA) only scales on CPU/memory and cannot go below 1 replica — so it can neither react to event sources (queue depth, HTTP backlog) nor ever scale to zero. *KEDA (Kubernetes Event-Driven Autoscaling, a CNCF graduated project) adds both. The mental model is two-phase: the KEDA operator itself owns the 0↔1 transition (it directly sets a Deployment to 0 replicas when idle and back to 1 when a scaler's activation threshold is crossed — the thing HPA cannot do), and from 1→N it creates a standard HPA and hands off, with KEDA's metrics-apiserver feeding the HPA event-source metrics via the Kubernetes external-metrics API. KEDA wraps HPA; it does not replace it. You declare a ScaledObject that ties a Deployment to triggers[] (one of 60+ scalers: RabbitMQ queue length, Kafka lag, Prometheus query, Redis list length, cron, AWS SQS…), minReplicaCount: 0 for scale-to-zero, maxReplicaCount, cooldownPeriod (default 300s idle before scaling back to 0), and pollingInterval. Each scaler has an activation threshold (decides 0→1) distinct from its target (drives 1→N). HTTP is the special case: a queue/Prometheus metric exists whether or not the app runs, so KEDA can poll it at zero replicas — but at 0 pods nothing receives an HTTP request to produce a metric, so you need the keda-http-add-on: an interceptor proxy in the request path that buffers the incoming request, signals KEDA to scale 0→1, waits for the pod's readiness probe, then forwards the buffered request (and counts pending requests as the 1→N metric), plus an external scaler and the HTTPScaledObject CRD. You wire it by pointing the service's Ingress backend at the interceptor instead of the app Service. Cold start is the first request after idle (schedule → container start → readiness, ~2–5s for .NET, less with ReadyToRun/AOT) and the interceptor buffers it into a slow 200 rather than a 502. Judgment: HPA = CPU/mem only, min 1; KEDA = event-driven + scale-to-zero, lightweight (wraps HPA, ~50–100 Mi); Knative also scales to zero but ships a far heavier control plane (activator, autoscaler, per-pod queue-proxy) that on a tight node can cost more than it saves — which is why KEDA's http-add-on is preferred there. The consumer nuance: a queue consumer scaled to zero by a non-queue trigger would stop draining its queue — but KEDA's idiomatic pattern is the queue scaler (e.g. rabbitmq queueLength), so the consumer scales 0→N on queue depth: it sleeps at 0 and wakes exactly when messages arrive, so with the right* scaler a consumer can scale to zero (conservative first pass is still minReplicaCount: 1). Stateful workloads (databases, brokers, caches) are never KEDA targets. What it buys you: idle workloads drop to 0 replicas → 0 RAM and 0 scheduling reservation, waking on demand — the density lever. Caveat to teach: scale-to-zero gives 0 RAM per idle container, not a $0 hardware bill (a self-hosted node costs the same idle or full); literal $0-at-idle is per-request serverless (Cloud Run/Lambda/Fly).