keda — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited keda (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
KEDA extends Kubernetes HPA with event-driven scalers (queues, cron, Prometheus, etc.) and owns the 0 ↔ 1 transition so workloads can truly scale to zero. The skill covers three CRDs (ScaledObject, ScaledJob, TriggerAuthentication), 79 documented scalers (counted at v2.21), HPA behavior tuning, and the gotchas that make production KEDA misbehave.
This file holds the mental model and the 80% patterns. Reach for the files in references/ for depth.
KEDA and the built-in HPA divide responsibility:
| Transition | Owner | Mechanism |
|---|---|---|
0 → 1 activation | KEDA operator | Polls triggers every pollingInterval (default 30s). Any active trigger wakes the workload. |
1 → N scale-up | HPA (managed by KEDA) | Reads external metrics via keda-operator-metrics-apiserver every ~15s. Replicas = ceil(sum(metric) / target). |
N → 1 scale-down | HPA | Damped by behavior.scaleDown.stabilizationWindowSeconds (default 300s). |
1 → 0 deactivation | KEDA operator | All triggers inactive for cooldownPeriod (default 300s). |
For each ScaledObject, KEDA creates a managed HPA named keda-hpa-<scaledobject-name>. Don't create a second HPA on the same target — it conflicts. If one already exists, KEDA's admission webhook rejects the ScaledObject until the manual HPA is deleted (or adopted via annotation scaledobject.keda.sh/transfer-hpa-ownership: "true").
ScaledJob is different: no HPA. KEDA spawns new Job resources when triggers activate, and jobs run to completion — they are never killed to scale down.
| Workload | Use |
|---|---|
| Long-running service (web, consumer, worker) | ScaledObject |
| One event → one job that must complete uninterrupted | ScaledJob |
| HTTP traffic, scale on RPS or concurrency (inc. scale-to-zero) | KEDA HTTP Add-on (HTTPScaledObject) |
The killer case for ScaledJob: a long-running message handler whose pod is terminated mid-work by the HPA loses progress. Jobs are immune to that.
Adapt these. Every real ScaledObject is a variation on one of them.
apiVersion: keda.sh/v1alpha1
kind: TriggerAuthentication
metadata:
name: rabbitmq-auth
namespace: apps
spec:
secretTargetRef:
- parameter: host
name: rabbitmq-credentials
key: amqp-host
---
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: order-worker
namespace: apps
spec:
scaleTargetRef:
name: order-worker
pollingInterval: 30
cooldownPeriod: 300
minReplicaCount: 0
maxReplicaCount: 30
fallback:
failureThreshold: 3
replicas: 3
triggers:
- type: rabbitmq
metadata:
protocol: amqp
queueName: orders
mode: QueueLength
value: "20" # target: 20 messages per replica
activationValue: "5" # wake from 0 at 5 messages
authenticationRef:
name: rabbitmq-authTwo triggers in a ScaledObject combine as max of desired replicas, not sum. Whichever trigger wants more pods wins.
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: order-api
namespace: apps
spec:
scaleTargetRef:
name: order-api
minReplicaCount: 3
maxReplicaCount: 50
advanced:
horizontalPodAutoscalerConfig:
behavior:
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Percent
value: 100
periodSeconds: 30
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 10
periodSeconds: 60
triggers:
- type: prometheus
name: rps
metadata:
serverAddress: http://prometheus.monitoring.svc:9090
query: sum(rate(http_requests_total{service="order-api"}[1m]))
threshold: "200"
ignoreNullValues: "true"
- type: cpu
metricType: Utilization
metadata:
value: "70"Overlapping cron triggers combine as max. Use desiredReplicas as a floor — if other triggers demand more, they win.
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: inference
namespace: ml
spec:
scaleTargetRef:
name: inference
minReplicaCount: 1
maxReplicaCount: 12
triggers:
- type: cron
metadata:
timezone: Europe/Stockholm
start: "0 7 * * 1-5"
end: "0 18 * * 1-5"
desiredReplicas: "10"
- type: cron
metadata:
timezone: Europe/Stockholm
start: "0 18 * * 1-5"
end: "0 22 * * 1-5"
desiredReplicas: "5"apiVersion: keda.sh/v1alpha1
kind: ScaledJob
metadata:
name: video-encoder
spec:
jobTargetRef:
parallelism: 1
completions: 1
backoffLimit: 2
template:
spec:
restartPolicy: Never
containers:
- name: encoder
image: encoder:v1
pollingInterval: 30
maxReplicaCount: 20
successfulJobsHistoryLimit: 5
failedJobsHistoryLimit: 10
scalingStrategy:
strategy: accurate # queueLength - runningJobs; avoids over-provisioning
triggers:
- type: aws-sqs-queue
metadata:
queueURL: https://sqs.us-east-1.amazonaws.com/123/videos
queueLength: "1" # 1 message per job
awsRegion: us-east-1
authenticationRef:
name: aws-irsaLatest stable is v2.21.0 (2026-09-23). Read the 2.21 breaking changes in full before choosing a target.
Every release ≤ 2.20.2 is exposed to GHSA-637c-6jxx-4rwm (critical). A tenant with create on TriggerAuthentication + ScaledObject in its own namespace sets hashiCorpVault.address to its own URL and receives the operator's ServiceAccount token: cluster-wide Secret read, namespace admin → cluster admin. Vault use is irrelevant. Fixed only in 2.21.0; no 2.20 backport.
The fix breaks Vault Kubernetes auth and all `boundServiceAccountToken` auth. 2.21 defaults to serviceAccountTokens.mode: enforce-audience; a token without an approved audience fails per scaler while the operator stays healthy. Before upgrading: find TA/CTAs using hashiCorpVault.authentication: kubernetes or boundServiceAccountToken; give each an audience the API server rejects (chart: hashiCorpVault.kubernetesAuth.audience (default vault), operator.serviceAccountTokens.additionalAllowedAudiences, permissions.operator.restrict.serviceAccountTokenCreationRoles[].audience); make the receiver accept it; upgrade chart and operator together. legacy mode is the explicit insecure bypass. Steps: keda.sh/docs/2.21/migration/.
Also breaking in 2.21: Temporal buildId, selectAllActive, selectUnversioned are removed — objects using them fail metadata parsing; move to workerDeploymentName / workerDeploymentBuildId. Azure Pipelines gains scaleOnInFlight (default true): queue length now counts assigned, unfinished jobs; set false for 2.20 behaviour (ScaledJob: with scalingStrategy: accurate).
1. Grant RBAC on `events.k8s.io` first. v2.20.0 moved Kubernetes event recording from the legacy core events resource to the `events.k8s.io` API group, following the Kubernetes 0.35 dependency bump. If you run KEDA with custom or restricted RBAC, grant the operator create and patch on events.k8s.io/events before upgrading, or event recording fails. The bundled manifests and Helm chart already carry it. Note v2.20.2 then had to restore the core "" API group as well (#7922), because client-go's legacy event broadcaster still needs it — so a hand-rolled Role wants both.
2. Four deprecated scaler fields were removed in v2.20.0, and a manifest still using them breaks:
| Scaler | Removed | Use instead |
|---|---|---|
| GCP PubSub | subscriptionSize | mode + value (#7720) |
| Huawei Cloudeye | minMetricValue | activationTargetMetricValue (#7436) |
| InfluxDB | authToken in triggerMetadata | authToken via resolvedEnv or authParams (#7722) |
| IBM MQ | the tls setting | removed outright (#6094) |
Security: GHSA-6w3m-4hhp-775q (medium, 2026-06-01) is a connection-string parameter injection in the PostgreSQL scaler, affecting ≤ 2.19.x and patched in 2.20. If you use that scaler and are below 2.20, that is the upgrade reason.
Kubernetes compatibility is a tested window, not a floor. KEDA tests N-2 minors: v2.21 covers 1.34–1.36, v2.20 covers 1.33–1.35, v2.19 covers 1.32–1.34. Kubernetes 1.36 needs 2.21; 1.33 is out of 2.21's tested window.
deletes left ScaledObjects in Terminating. Not reproduced on 2.20.2, no fix PR named. If it recurs, a manual kubectl patch removing the finalizer clears it.
registered (#8039), while still passing its liveness probe. Open at time of check.
Every item here has cost people incidents. Internalize them.
1. `cooldownPeriod` only governs `N → 0`, not `N → 1`. Scale-down from 10 pods to 1 is controlled entirely by HPA's behavior.scaleDown.stabilizationWindowSeconds (default 300s). Setting cooldownPeriod: 1800 does not slow N→1 scale-down. Configure both.
2. CPU and memory scalers cannot drive scale-to-zero alone. HPA requires minReplicas ≥ 1 for resource metrics — no pods means no CPU signal to wake them. Pair CPU/memory with a secondary scaler (cron, a queue trigger, prometheus) that can evaluate without running pods.
3. `activationThreshold` is ignored when `minReplicaCount ≥ 1`. It only gates the 0 → 1 transition. If minReplicaCount: 1, setting a high activationThreshold does nothing — pods are always running, so activation is always true. Don't try to use it as a second scale-down threshold.
4. Multiple triggers on a ScaledObject combine as `max`, not sum. If Kafka lag suggests 10 pods and CPU suggests 5, the result is 10. For a weighted combination or ratio, use advanced.scalingModifiers.formula (KEDA 2.13+). See references/patterns.md.
5. `idleReplicaCount` only works with the value `0`. Other values have HPA compatibility issues. Use it to run e.g. 2 pods while active but fully scale to zero when idle: idleReplicaCount: 0 + minReplicaCount: 2.
6. Manual HPA on the same target blocks the ScaledObject. Check kubectl describe scaledobject. Delete the manual HPA to resolve, or set annotation scaledobject.keda.sh/transfer-hpa-ownership: "true".
7. One external-metrics provider per cluster. The API external.metrics.k8s.io can only be served by one component at a time. If Datadog Cluster Agent or Prometheus Adapter is already registered, KEDA's metrics-apiserver fails silently. Pick one.
8. `fallback` does not work for cpu/memory triggers. It requires metricType: AverageValue, which resource metrics lack. Use fallback on external-metric triggers only (Prometheus, RabbitMQ, etc.).
9. Always set a `fallback` on production external-metric triggers. When Prometheus/Kafka/etc. is unreachable for failureThreshold polls in a row, KEDA injects the fallback replica count rather than leaving the deployment flapping to minReplicaCount.
10. Don't poll aggressively against shared metric sources. pollingInterval: 5 across 50 ScaledObjects = 600 queries/min against one Prometheus. 30s is the sane default; drop below only with a reason. From 2.21, with minReplicaCount > 0, no idleReplicaCount and no useCachedMetrics, the operator stops polling and the HPA's 15s cycle is the only querier — pollingInterval no longer adds load there.
11. HPA behavior stabilization windows must be multiples of the HPA sync period (15s). Use 15s, 30s, 60s, 300s — not 20s or 100s. Non-aligned windows lead to non-deterministic decisions.
12. `useCachedMetrics: true` on triggers reduces scaler load from the HPA's 15s sync cycle by reusing cached values within the pollingInterval. Not available for cpu, memory, or cron scalers.
Annotations on a ScaledObject (not TriggerAuthentication):
| Annotation | Effect |
|---|---|
autoscaling.keda.sh/paused: "true" | Freeze current replica count. Metrics still collected but HPA not reconciled. |
autoscaling.keda.sh/paused-replicas: "5" | Pin to exactly 5 replicas until removed. |
autoscaling.keda.sh/paused-scale-in: "true" | Block scale-down only (HPA scaleDown → Disabled). |
autoscaling.keda.sh/paused-scale-out: "true" | Block scale-up only. |
scaledobject.keda.sh/transfer-hpa-ownership: "true" | Adopt an existing HPA rather than conflict. |
autoscaling.keda.sh/force-activation: "true" | Force all scalers active immediately (break-glass). |
Run these three first — 90% of issues surface here:
# 1. Is the ScaledObject Ready? What reason?
kubectl describe scaledobject <name> -n <ns>
# 2. Was the HPA created?
kubectl get hpa keda-hpa-<name> -n <ns> -o yaml
# 3. Operator saying anything?
kubectl logs -n keda deploy/keda-operator --tail=300 | grep <name>If all three are clean, the problem is not KEDA itself but the trigger's event source. Steps 4-6 — external metrics API health, serving the metric, and metrics-apiserver logs — are in references/troubleshooting.md, which also carries a decision tree mapping symptoms to root causes.
The helper ${CLAUDE_SKILL_DIR}/scripts/debug-scaledobject.sh <name> [namespace] runs all six in one shot.
references/scalers.md (catalog ofevery scaler with YAML snippets and field-level defaults).
references/crds.md.
Workload Identity, Vault, Key Vault, Secrets Manager), observability** → references/operations.md.
cron overlap semantics, HTTP Add-on, GitOps with Argo/Flux, Karpenter interplay, production hardening** → references/patterns.md.
references/troubleshooting.md.
references/sources.md.
When writing or reviewing a ScaledObject, tick these:
(ScaledJob)? Long-running + terminate-mid-work = wrong shape.
minReplicaCount: 0 actually safe for this workload? Cold-startcost, warmup probes, first-request latency all acceptable?
trigger to enable scale-to-zero (or is minReplica ≥ 1 intentional)?
maxReplicaCount set to a number the cluster/node pool can actuallyprovision? Karpenter/CA can provision in time?
fallback configured on external-metric triggers?advanced.horizontalPodAutoscalerConfig.behavior tuned for thisworkload? Default scaleDown of 300s too aggressive for a 60s-warmup pod?
TriggerAuthentication (namespace) orClusterTriggerAuthentication (cross-namespace); cloud podIdentity preferred over static secrets.
name whenscalingModifiers is in use.
pollingInterval ≥ 30s unless the event source is local and cheap.keda_scaler_errors_total and keda_scaled_object_errors_total?Key settings when hardening KEDA itself (see references/operations.md for the full set):
KEDA_RESTRICT_SECRET_ACCESS=true — operator only reads secrets in the kedanamespace (forces ClusterTriggerAuthentication for cross-namespace secrets).
WATCH_NAMESPACE=team-a,team-b — operator reconciles only listed namespaces.--kube-api-qps=50 --kube-api-burst=75 — raise client throttle at scale.failurePolicy: Fail for strict validation (catch invalidScaledObjects at submit time).
The main KEDA project does not scale HTTP workloads by RPS on its own. The HTTP Add-on (a separate deploy, API group http.keda.sh/v1alpha1, CRD HTTPScaledObject) does. As of v0.16.0 it's still beta — the README calls it stable and actively maintained with a v1.0 planned, but pre-1.0 breaking changes are still on the table. For an HTTP-scaling pattern today, either:
prometheus trigger, orDetails in references/patterns.md.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.