Skip to content
Go back

KEDA: Event-Driven Autoscaling Self-Hosted

By SumGuy 9 min read
KEDA: Event-Driven Autoscaling Self-Hosted
Contents

HPA Is Great, Until It Isn’t

You’ve got Kubernetes running. Maybe k3s on a couple of mini PCs. You’ve wired up the Horizontal Pod Autoscaler and watched it scale your app from 2 pods to 6 when CPU hits 70%. Nice.

Then reality shows up.

Your video transcoding job needs to kick off at 3 AM. Your image-processing worker should spin up when Frigate catches motion on the security cam. Your queue worker should scale based on how many rows are sitting in a Postgres table, not whether the CPU is sweating.

HPA out of the box only understands CPU and memory. That’s like hiring a bouncer who only checks height, it works until the situation requires actual judgment.

That’s where KEDA comes in.


What KEDA Actually Is

KEDA (Kubernetes Event-Driven Autoscaling) is a CNCF graduated project that extends HPA with a massive catalog of event sources, called scalers. Instead of asking “how hot is the CPU?”, KEDA asks “how many messages are in the queue?” or “is it 3 AM on a Tuesday?”

The architecture is straightforward:

You define a ScaledObject that points at your Deployment and a trigger (or multiple triggers). KEDA handles the rest, including the magic trick: scale to zero and back.

HPA can’t scale below 1 replica. KEDA owns the 0→1 transition itself, then hands off to HPA for 1→N. For a home lab where you don’t want idle workers burning RAM at 2 PM, this is huge.


Installing KEDA

Helm is the standard path. Add the repo and install into its own namespace:

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

Verify it’s up:

Terminal window
kubectl get pods -n keda

You should see three pods: keda-operator, keda-operator-metrics-apiserver, and keda-admission-webhooks. All three need to be Running before you start creating ScaledObjects.

That’s honestly it for the install. KEDA is one of those tools that gets out of your way quickly.


The ScaledObject CRD

Every KEDA setup revolves around a ScaledObject. Here’s the anatomy:

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: my-worker
namespace: default
spec:
scaleTargetRef:
name: my-worker-deployment # the Deployment to scale
minReplicaCount: 0 # scale to zero — saves resources
maxReplicaCount: 10
cooldownPeriod: 300 # seconds before scaling back to 0 (default 300)
pollingInterval: 30 # how often KEDA checks the scaler (seconds)
triggers:
- type: rabbitmq # the scaler
metadata:
protocol: amqp
queueName: jobs
host: amqp://user:[email protected]:5672/
queueLength: "5" # one pod per 5 messages in queue

The cooldownPeriod is your friend. It prevents flapping, if your queue drains and refills every 45 seconds, you don’t want pods appearing and vanishing like ghosts. 300 seconds (5 minutes) is a reasonable default; tune it down if your workloads start and stop cleanly in seconds.


Home Lab Use Cases (With Actual YAML)

Cron Trigger: Batch Transcoding at 3 AM

You’ve got Jellyfin, a pile of rips, and a transcoding worker that you absolutely do not want running during the day consuming RAM. Cron trigger:

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: transcode-batch
namespace: media
spec:
scaleTargetRef:
name: transcode-worker
minReplicaCount: 0
maxReplicaCount: 4
cooldownPeriod: 600
triggers:
- type: cron
metadata:
timezone: "America/New_York"
start: "0 3 * * *" # 3 AM
end: "0 5 * * *" # 5 AM
desiredReplicas: "4"

Between 3 to 5 AM, four workers spin up and chew through the queue. At 5 AM, they scale back to zero. Your home lab CPU is free the rest of the day. Your 2 AM self doesn’t have to remember to run anything.


MQTT Trigger: Frigate Motion Events

This one’s genuinely cool for home automation nerds. You’ve got Frigate detecting motion, publishing to an MQTT broker (Mosquitto). A computer vision worker should spin up only when there’s something to process.

First, store the MQTT connection as a secret:

apiVersion: v1
kind: Secret
metadata:
name: mqtt-secrets
namespace: vision
type: Opaque
stringData:
mqttHost: "mqtt://mosquitto.home.svc.cluster.local:1883"

Then the ScaledObject:

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: frigate-processor
namespace: vision
spec:
scaleTargetRef:
name: motion-worker
minReplicaCount: 0
maxReplicaCount: 3
cooldownPeriod: 120
triggers:
- type: mqtt
metadata:
brokerList: "mosquitto.home.svc.cluster.local:1883"
topic: "frigate/events"
qosLevel: "1"
messageCount: "1"
authenticationRef:
name: mqtt-trigger-auth

One message in the topic, one pod spins up. Camera quiet for two minutes, pod goes away. For a home lab this is the kind of thing that makes you feel like you’ve actually built something clever.


Postgres Trigger: Queue Table Worker

You’ve got a jobs table. Workers poll it. You want more workers when the backlog grows. No Kafka, no Redis, just Postgres rows.

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: job-queue-worker
namespace: app
spec:
scaleTargetRef:
name: job-processor
minReplicaCount: 0
maxReplicaCount: 8
triggers:
- type: postgresql
metadata:
query: "SELECT COUNT(*) FROM jobs WHERE status = 'pending'"
targetQueryValue: "10" # one pod per 10 pending rows
dbName: "appdb"
sslmode: "disable"
host: "postgres.app.svc.cluster.local"
port: "5432"
userName: "worker_user"
passwordFromEnv: DB_PASSWORD

When pending jobs exceed 100 rows, you’ll have 10 workers. Queue drains to zero? Workers go away. It’s the most boring architecture possible, and it works perfectly.


Redis Streams Trigger: Bull/RQ Workers

If you’re running Node.js Bull queues or Python RQ backed by Redis, KEDA speaks Redis Streams natively:

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: rq-worker-scaler
namespace: workers
spec:
scaleTargetRef:
name: rq-worker
minReplicaCount: 0
maxReplicaCount: 6
cooldownPeriod: 180
triggers:
- type: redis-streams
metadata:
address: "redis.workers.svc.cluster.local:6379"
stream: "job-stream"
consumerGroup: "rq-workers"
pendingEntriesCount: "5"

pendingEntriesCount: "5" means one pod per 5 messages that have been delivered to the consumer group but not acknowledged yet. Real backpressure, not fake CPU-based guessing.


ScaledJob: For Fan-Out Batch Work

ScaledObject scales a long-running Deployment. ScaledJob is for workloads that should run once per event, think: one Kubernetes Job per message.

apiVersion: keda.sh/v1alpha1
kind: ScaledJob
metadata:
name: photo-resize-job
namespace: media
spec:
jobTargetRef:
template:
spec:
containers:
- name: resize
image: myrepo/photo-resizer:latest
restartPolicy: Never
minReplicaCount: 0
maxReplicaCount: 20
triggers:
- type: rabbitmq
metadata:
queueName: photo-uploads
host: amqp://rabbitmq.default.svc.cluster.local:5672/
queueLength: "1"

Each photo upload message in RabbitMQ spawns one Job pod. Pod runs, resizes, exits. No persistent worker sleeping between requests. For batch workloads where the work unit is discrete and you want parallelism, ScaledJob is the right model.


The Scale-to-Zero Catch: Cold Start Latency

Here’s the honest part nobody leads with.

Scale to zero sounds amazing. It is amazing. Until a user hits your service and waits 15 seconds while a container starts up.

Typical cold start times:

For background workers processing queues, this is fine. Nobody’s watching the pod start. For HTTP request handlers, it’s user-facing latency.

Two mitigation options:

1. KEDA HTTP Add-on, intercepts HTTP requests during cold start and holds them in a buffer while the pod initializes. The caller’s connection stays open; the request isn’t lost. Install it separately:

Terminal window
helm install http-add-on kedacore/keda-add-ons-http \
--namespace keda

Then use HTTPScaledObject instead of ScaledObject for HTTP-triggered services.

2. Pod Pre-warming / minReplicaCount: 1, honestly, for latency-sensitive services in a home lab, just set minReplicaCount: 1. You keep one pod warm at all times, skip cold starts entirely, and still let KEDA scale out to N under load. You’re not running a serverless platform that bills per millisecond.

Scale to zero is most valuable for batch workers, scheduled jobs, and event-driven processors where nobody’s watching the clock.


Prometheus Trigger: Scaling on Anything

If you’ve got Prometheus running (and if you’re reading this article, you probably do or should), you can scale on any metric that Prometheus scrapes. Request rate, error rate, custom business metrics, whatever.

triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus.monitoring.svc.cluster.local:9090
metricName: http_requests_in_flight
query: sum(rate(http_requests_total{job="api"}[2m]))
threshold: "100" # one pod per 100 req/s

This is the power move. You’ve got full observability from Prometheus and you’re using it as the autoscaling brain. If your SLO dashboard is already on a Grafana wall, your scaling policy lives in the same place your alerts do.


A Note on KNative

KEDA and KNative Serving can coexist, and for pure “scale from zero on HTTP” use cases, KNative is often cleaner. KNative Serving handles cold starts, request buffering, and traffic splitting out of the box.

But KNative brings significant operational overhead, it’s a platform on top of a platform. For a home lab running k3s on commodity hardware, KEDA + the HTTP add-on covers 95% of the same ground without pulling in the KNative stack. Unless you’re already invested in KNative, start with KEDA.


Should You Bother?

If you’re running k3s on a box with 16 to 32 GB RAM and you’ve got background workers, cron jobs, or event-driven workloads: yes, absolutely bother.

The install is 10 minutes. The first ScaledObject YAML takes another 15. And then you get workers that actually sleep when there’s nothing to do, spin up when there is, and don’t require you to remember to run a cron job manually or size your Deployment based on peak-load guesses.

The scaler catalog in 2026 is deep: RabbitMQ, Kafka, Redis, Redis Streams, Postgres, MySQL, MQTT, Prometheus, Cron, NATS, AWS SQS, GCP PubSub, Azure Service Bus, GitHub Runners, and more. Odds are your event source is already supported.

Where it doesn’t make sense: simple stateless HTTP services with consistent traffic patterns. HPA on CPU/memory is genuinely fine for those, and adding KEDA is complexity without payoff.

For everything else, the batch jobs, the queue workers, the home automation triggers, the scheduled tasks, KEDA is one of those tools that makes you wonder how you ran Kubernetes without it.

Scale to zero. Wake on demand. Go touch grass.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Next Post
Beszel: Server Monitoring Without the Prometheus Tax

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts