KEDA — Kubernetes Event-Driven Autoscaling — solves a problem that standard Kubernetes Horizontal Pod Autoscaler cannot: scaling workloads based on the depth of a message queue, the lag on a Kafka topic, or the number of rows in a database table. For IoT data pipelines and edge Kubernetes clusters, where work arrives in bursts and idle periods need to cost nothing, KEDA’s scale-to-zero capability is the enabling technology that makes event-driven architectures on Kubernetes practical.

This guide covers KEDA’s architecture, the scalers most relevant to IoT and edge deployments, and practical configuration for common patterns.

What KEDA Does

Standard Kubernetes HPA scales pods based on CPU and memory metrics. This works for request-driven web services but fails for queue consumers: if your pod isn’t processing anything, CPU is idle regardless of how many messages are queued. HPA sees low CPU and does nothing. The queue backs up.

KEDA introduces a third scaling mode alongside HPA and VPA: event-driven scaling. A KEDA ScaledObject watches an external source — an MQTT subscription, a Kafka consumer group, an Azure Service Bus queue, a Prometheus metric, a PostgreSQL query result — and scales the target deployment based on what it finds there.

Crucially, KEDA scales to zero when the source is empty. When messages arrive, it scales from zero to the configured minimum. This eliminates the cost of idle consumer pods, which is particularly valuable at the edge where compute resources are constrained and billing is often consumption-based.

Architecture

KEDA runs as two components in your cluster:

  • keda-operator: watches ScaledObject and ScaledJob resources and manages the HPA that drives scaling
  • keda-metrics-apiserver: exposes external metric values to Kubernetes as a custom metrics API so HPA can act on them
[External Source (MQTT/Kafka/DB)]

[KEDA keda-operator polls source]

[Writes metric to keda-metrics-apiserver]

[HPA reads external metric via Custom Metrics API]

[Scales target Deployment/StatefulSet]

KEDA installs via Helm:

helm repo add kedacore https://kedacore.github.io/charts
helm repo update
helm install keda kedacore/keda \
  --namespace keda \
  --create-namespace

ScaledObject Basics

A ScaledObject binds a scaling target (a Deployment, StatefulSet, or custom resource) to a trigger (the event source). Here’s the structure:

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: iot-processor-scaledobject
spec:
  scaleTargetRef:
    name: iot-data-processor     # Deployment name
  minReplicaCount: 0             # Scale to zero when idle
  maxReplicaCount: 20            # Never exceed 20 pods
  pollingInterval: 15            # Check source every 15 seconds
  cooldownPeriod: 60             # Wait 60s after last event before scaling down
  triggers:
    - type: mqtt                 # Trigger type
      metadata:
        ...

minReplicaCount: 0 is the scale-to-zero setting. When the trigger reports zero events pending, KEDA removes all pods. When new events arrive, KEDA scales from zero to one pod (bypassing HPA’s minimum) and then lets HPA scale further based on the metric.

MQTT Trigger for IoT Sensor Data

MQTT is the dominant protocol for IoT sensor data, and KEDA’s MQTT scaler monitors topic subscription depth. This is particularly useful when sensor devices publish at variable rates and you want processing pods to appear only when data is waiting.

The MQTT scaler requires a ScaledObject and a TriggerAuthentication for broker credentials:

apiVersion: v1
kind: Secret
metadata:
  name: mqtt-secret
stringData:
  username: "keda-consumer"
  password: "your-mqtt-password"
---
apiVersion: keda.sh/v1alpha1
kind: TriggerAuthentication
metadata:
  name: mqtt-trigger-auth
spec:
  secretTargetRef:
    - parameter: username
      name: mqtt-secret
      key: username
    - parameter: password
      name: mqtt-secret
      key: password
---
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: sensor-processor-scaler
spec:
  scaleTargetRef:
    name: sensor-data-processor
  minReplicaCount: 0
  maxReplicaCount: 10
  pollingInterval: 10
  cooldownPeriod: 120
  triggers:
    - type: mqtt
      authenticationRef:
        name: mqtt-trigger-auth
      metadata:
        brokerList: "tcp://mosquitto.iot-system.svc.cluster.local:1883"
        topic: "sensors/temperature"
        qos: "1"
        lagThreshold: "5"   # Scale up when >5 messages are pending per replica

lagThreshold controls the scaling sensitivity. With lagThreshold: "5", KEDA targets one replica per 5 pending messages — a queue of 25 messages triggers 5 replicas.

Kafka Scaler for High-Throughput IoT Streams

For high-throughput IoT scenarios using Kafka (or Redpanda) as the message bus, the Kafka scaler measures consumer group lag — the difference between the latest offset and the consumer’s current offset.

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: iot-kafka-processor
spec:
  scaleTargetRef:
    name: kafka-stream-processor
  minReplicaCount: 0
  maxReplicaCount: 30
  pollingInterval: 15
  triggers:
    - type: kafka
      metadata:
        bootstrapServers: "kafka.iot-platform.svc.cluster.local:9092"
        consumerGroup: "iot-data-processors"
        topic: "raw-sensor-data"
        lagThreshold: "100"      # One replica per 100 messages of lag
        offsetResetPolicy: "latest"

For Kafka deployments with TLS and SASL authentication, KEDA’s TriggerAuthentication supports the full range of Kafka authentication mechanisms.

The Kafka scaler is well-suited to edge Kubernetes clusters running near IoT gateways, where you want processors to consume sensor telemetry within seconds of arrival but idle to zero when sensor networks are quiet (overnight, weekend, planned maintenance windows).

PostgreSQL / TimescaleDB Trigger

Many IoT platforms land sensor data in a time-series database (TimescaleDB, regular PostgreSQL with partitioning, or InfluxDB) and then need batch processing or alerting workers to process unprocessed rows. The PostgreSQL scaler runs a query against the database and scales based on the returned value.

apiVersion: keda.sh/v1alpha1
kind: TriggerAuthentication
metadata:
  name: timescale-auth
spec:
  secretTargetRef:
    - parameter: connection
      name: timescale-secret
      key: connectionString
---
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: anomaly-detector-scaler
spec:
  scaleTargetRef:
    name: anomaly-detection-worker
  minReplicaCount: 0
  maxReplicaCount: 5
  pollingInterval: 30
  triggers:
    - type: postgresql
      authenticationRef:
        name: timescale-auth
      metadata:
        query: |
          SELECT COUNT(*) FROM sensor_readings
          WHERE processed = false
          AND reading_time > NOW() - INTERVAL '1 hour'
        targetQueryValue: "50"    # One replica per 50 unprocessed rows
        dbName: "iot_platform"
        sslmode: "require"

This pattern is useful for anomaly detection and alerting workers that should only run when there’s a meaningful backlog of unprocessed readings, and should idle to zero otherwise.

KEDA on k3s at the Edge

KEDA works well with k3s, the lightweight Kubernetes distribution popular for edge and IoT deployments. k3s runs on hardware as modest as Raspberry Pi 4 and ARM-based industrial gateways, and KEDA’s memory footprint is compatible with constrained edge nodes.

For an edge k3s cluster processing sensor data from a factory floor:

# Install k3s on edge node
curl -sfL https://get.k3s.io | sh -

# Install KEDA on the edge cluster
helm install keda kedacore/keda \
  --namespace keda \
  --create-namespace \
  --set operator.resources.requests.memory=64Mi \
  --set operator.resources.limits.memory=256Mi \
  --set metricsServer.resources.requests.memory=64Mi \
  --set metricsServer.resources.limits.memory=256Mi

Reducing resource requests for edge deployments ensures KEDA doesn’t crowd out workloads on nodes with limited RAM (4–8GB is common on edge hardware).

ScaledJob for Batch Processing

Beyond scaling Deployments, KEDA supports ScaledJob — which creates Kubernetes Jobs rather than scaling existing pods. This is ideal for IoT batch processing where each unit of work should run in an isolated, short-lived container.

apiVersion: keda.sh/v1alpha1
kind: ScaledJob
metadata:
  name: firmware-analysis-jobs
spec:
  jobTargetRef:
    template:
      spec:
        containers:
          - name: firmware-analyzer
            image: your-registry/firmware-analyzer:latest
            env:
              - name: MQTT_BROKER
                value: "tcp://mosquitto:1883"
        restartPolicy: Never
  triggers:
    - type: mqtt
      metadata:
        brokerList: "tcp://mosquitto.iot-system.svc.cluster.local:1883"
        topic: "firmware/updates/pending"
        lagThreshold: "1"   # One job per pending message
  maxReplicaCount: 10
  rollout:
    strategy: gradual   # Don't delete running jobs when scaling down

ScaledJob with lagThreshold: "1" creates exactly one Job per pending message — each firmware update is processed in its own isolated container, and the containers disappear when done.

Observability

KEDA exposes Prometheus metrics at the /metrics endpoint on the operator and metrics server pods. The most useful metrics for IoT deployments are:

  • keda_scaled_object_errors_total — trigger evaluation failures
  • keda_resource_totals — count of ScaledObjects by state
  • External metric values (source-specific, e.g. Kafka lag, queue depth) are visible as Kubernetes custom metrics

For real-time visibility into scaling behaviour, the KEDA dashboard (available as a Grafana dashboard at dashboard ID 16907) shows target replicas, current replicas, and trigger values over time.

Common Pitfalls

Scale-to-zero and cold start latency: When a deployment scales from zero, the time to schedule a pod, pull the container image, and start the process adds latency to the first message processed after an idle period. For latency-sensitive IoT scenarios, consider minReplicaCount: 1 to keep at least one warm pod rather than scaling to zero.

Polling interval vs. latency: KEDA checks the trigger source on pollingInterval. A 30-second polling interval means up to 30 seconds between message arrival and scale-up initiation. For time-sensitive processing, reduce pollingInterval to 5–10 seconds — but be aware this increases the number of API calls to the trigger source.

Cooldown and flapping: Set cooldownPeriod long enough that brief idle gaps between message bursts don’t cause constant scale-up/scale-down cycles. For IoT workloads with periodic sensor bursts, 120 to 300 seconds is a reasonable starting point.

References