Introduction
In many teams, autoscaling in Kubernetes is still treated as “HPA on CPU%”. That works fine for a subset of stateless web workloads, but it breaks down (or scale-in/out becomes unstable) when CPU and memory are no longer a good proxy for workload pressure. KEDA exists exactly in that gap: workloads scale based on events and external signals (queue depth, stream lag, backlog, custom metrics), including the ability to scale to zero replicas when there is no work. KEDA does not replace HPA; it builds on top of it and uses Kubernetes scaling and metrics APIs as the interface between event sources and the existing autoscaling control loop.
Why autoscaling goes beyond CPU and memory
What the HPA actually does
The Kubernetes Horizontal Pod Autoscaler (HPA) is an API object plus controller that periodically adjusts the desired replica count of a scale target (for example a Deployment or StatefulSet) so measured metrics stay near a target value. That “periodic” behavior is explicit: Kubernetes implements HPA as a control loop running on a sync period (default 15 seconds), so it is always reactive to observations, not continuously event-driven in the strict sense.
HPA retrieves metrics through different paths, depending on metric type: resource metrics (CPU/memory) via the resource metrics API, and other metrics via the custom metrics API or external metrics API. In practice this means you typically need Metrics Server for CPU/memory, and one or more metrics adapters that implement those APIs for custom/external metrics.
The scaling decision itself is conceptually straightforward: the controller uses a ratio between current metric value and desired metric value and scales to ceil(currentReplicas * currentMetricValue / desiredMetricValue), with tolerances and stabilization mechanisms to dampen flapping.
Why CPU and memory are often poor proxies for workload pressure
CPU and memory are resource consumption signals, not necessarily work signals. That difference becomes obvious when the bottleneck is elsewhere.
In I/O-bound services, workload pressure can increase (more requests, more queueing) while CPU stays relatively low. With upstream/back-end latency or rate limits, throughput can drop without any CPU increase. For these workloads, scaling on a metric that directly represents “work backlog” (for example queue length, inflight requests, consumer lag) is often more useful than scaling on CPU%.
Even when CPU does correlate with load, HPA resource scaling depends on a model assumption: CPU utilization is calculated as a percentage of the resource request. If containers have no requests set, the autoscaler cannot act on that metric. That makes “HPA on CPU%” not only a proxy problem, but also a configuration and discipline problem (requests/limits).
The step to event-driven and metric-driven scaling
Kubernetes explicitly supports multiple metric types, including object metrics and external metrics. External metrics are meant for metrics not tied to a Kubernetes object (for example queue depth in an external broker) and are exposed via external.metrics.k8s.io.
In theory, “HPA + a metrics adapter” can already get you far. In practice, friction usually appears in three areas:
- exposing event-source metrics to Kubernetes metrics APIs consistently and securely
- modeling “activation” (0→1) versus normal scaling (1→N)
- managing a broad catalog of event sources (queues, streams, CI/CD queues, databases, custom APIs) without maintaining custom adapters every time
KEDA is positioned exactly at that intersection: not as a new scheduler, but as a control-plane extension that translates event sources into HPA-consumable metrics and also handles the 0↔1 activation phase.
What KEDA is and why it exists
Definition and core idea
KEDA (Kubernetes Event-driven Autoscaling) is a Kubernetes-native autoscaling component that can scale workloads based on “real-world events” such as queue depth or incoming request rate. It is designed to run alongside standard Kubernetes components, especially HPA, and expands the set of signals you can scale on without building a custom integration for every event source.
In the KEDA model, you define intent via Custom Resources (for example ScaledObject and ScaledJob), and define one or more triggers (scalers) that fetch metrics from an event source. KEDA translates those into autoscaling metrics.
Project status and design philosophy
KEDA is a project under the Cloud Native Computing Foundation and is now “Graduated” within CNCF maturity levels (acceptance 2020, incubating 2021, graduated 2023). That matters because autoscaling control-plane software directly affects stability and cost, and therefore benefits from mature governance and broad adoption.
The core philosophy: KEDA reuses existing Kubernetes primitives (HPA, metrics APIs, /scale subresource) instead of introducing a parallel autoscaling system. KEDA works with the grain of Kubernetes: CRDs for intent, controllers for reconcile loops, and the API aggregation layer for metrics.
Historically, KEDA started as a collaboration between Microsoft and Red Hat. That mainly says something about origin; the design is explicitly vendor-neutral (many event sources and authentication methods).
KEDA as autoscaling control plane, not scheduler
Important distinction: KEDA does not decide where a Pod runs and does not schedule Pods. It mainly influences how many replicas a workload should have, via the same scaling interfaces used by HPA. Scheduling stays fully with the Kubernetes scheduler; node capacity remains a separate layer (cluster/node autoscaling).
In deployment scaling mode, HPA is still responsible for 1→N scaling. KEDA mainly handles (a) 0→1 activation and (b) delivering external metrics to HPA.
KEDA architecture and behavior
Components and control loops
Conceptually, KEDA consists of a set of control-plane components:
- KEDA Operator: watches KEDA CRDs and manages the lifecycle of scaling configuration, including activating/deactivating workloads based on triggers.
- Metrics Server / Metrics Adapter: exposes external metrics to Kubernetes (primarily for HPA consumption) so HPA can scale on non-resource metrics.
- Scalers (triggers): integrations that fetch metrics from event sources; scalers also determine whether a workload is active (for 0↔1).
- Admission webhooks: validate (and in some cases mutate) KEDA resources to catch misconfigurations early.
Operationally this runs as multiple reconcile loops: the operator reconciles ScaledObjects/ScaledJobs, the metrics adapter serves requests through the Kubernetes API aggregation layer, and HPA reconciles desired replicas based on metrics.
ScaledObject: event-driven scaling for replica-based workloads
A ScaledObject links a scale target (usually a Deployment or StatefulSet) to one or more triggers. In the spec you define, among others:
scaleTargetRefto the object with a/scalesubresourceminReplicaCount(default 0) andmaxReplicaCountpollingInterval(default 30s) for checking the event source, especially relevant when replicas=0cooldownPeriod(default 300s), how long KEDA waits after the last active trigger before scaling back to 0
When you define a ScaledObject, the model is that KEDA monitors the event source and provides metrics to Kubernetes/HPA so HPA can perform 1→N scaling using the standard HPA controller.
A minimal pseudo-config (simplified, not intended as an install guide) looks like this:
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
spec:
scaleTargetRef:
name: worker-deployment
minReplicaCount: 0
maxReplicaCount: 50
pollingInterval: 30
cooldownPeriod: 300
triggers:
- type: <event-source>
metadata:
threshold: "<target>"
activationThreshold: "<activation>"
The semantics of threshold versus activationThreshold are important and return in event-driven scaling: KEDA distinguishes between the activation boundary (0↔1) and the regular HPA target (1↔N).
ScaledJob: event-driven scaling for job-based processing
For batch-like or worker code that fits Jobs better than a Deployment, KEDA provides ScaledJob. The explicit design goal is to start one Job per event (or per unit of work), process one event, and finish. KEDA docs position this as an alternative for long-running executions: instead of one deployment processing many events, you schedule one Job per event that runs to completion.
That model changes failure and lifecycle characteristics:
- With Deployments, Pods may be terminated during scale-down while still processing work (unless you implement graceful drain/shutdown well).
- With Jobs, the intent is run-to-completion; pods disappear only after work completes (or fails), so scale-down is mostly “stop creating new Jobs”, not direct termination.
KEDA’s ScaledJob spec also includes job-specific options, such as history limits for successful and failed jobs.
A simplified pseudo-config:
apiVersion: keda.sh/v1alpha1
kind: ScaledJob
spec:
maxReplicaCount: 100
pollingInterval: 30
triggers:
- type: <event-source>
metadata:
value: "<work-per-job>"
jobTargetRef:
template:
spec:
containers:
- name: worker
image: example/worker
restartPolicy: Never
Trigger mechanism and authentication
KEDA scalers do two things: determine whether a workload should be active (activation/deactivation) and provide metrics for scaling.
Authentication to event sources is often abstracted via TriggerAuthentication and ClusterTriggerAuthentication, so secrets/identity do not need to be repeated in every ScaledObject and governance becomes easier.
KEDA documentation also describes operational knobs relevant to reliability when calling external event sources, such as default HTTP timeouts (3 seconds) for HTTP-based scalers, and support for configuring proxies/timeouts through environment variables.
Metrics flow: HPA, metrics APIs, and the API aggregation layer
There are two overlapping polling rhythms in this system:
- the HPA controller polls metrics on its own sync period (default 15s)
- KEDA polls triggers per ScaledObject with
pollingInterval(default 30s), explicitly relevant for 0→1 scaling
That mismatch creates a practical issue: if HPA asks every 15s, you may not want to make an expensive external API call every 15s to a queue or monitoring system. Therefore KEDA supports metric caching (useCachedMetrics) so the metrics adapter can answer HPA requests with cached values collected on pollingInterval.
A second, more cluster-wide architecture point: the external metrics API (external.metrics.k8s.io) is exposed through Kubernetes API aggregation. In practice this means a cluster-wide APIService is registered. KEDA docs explicitly note that you can only have one active metrics server serving external.metrics.k8s.io, and that this role should be KEDA’s metrics server when using KEDA.
That is not a theoretical detail: if the APIService is not Available (for example because of network policies, service mesh side effects, or proxy settings), you can get errors such as FailedDiscoveryCheck, and multiple control-plane paths (HPA scaling, API discovery, sometimes namespace deletion) can be affected by the missing aggregated API.
Event-driven autoscaling explained
What is an “event” in this context?
“Event-driven autoscaling” in KEDA does not mean Kubernetes suddenly becomes a push-based scheduler. It means the scaling decision is driven by event-adjacent signals: queue length, pending jobs, consumer lag, backlog, inflight requests, or metrics produced by an event system. KEDA explicitly calls these “real-world events”, such as queue messages or incoming requests.
A useful engineering definition in this context:
An event is a discrete indication that work is waiting in a queue or that processing capacity is required, without first needing CPU/memory pressure to appear.
In real systems, most scaling signals are not discrete events but aggregate metrics (gauge/counter) over events: queue depth (gauge), lag (gauge), throughput (rate), or pending/inflight count. KEDA translates those into external metrics for HPA consumption.
Push versus pull signals
In standard KEDA scaling, the dominant mode is pull: KEDA polls event sources on an interval (pollingInterval) and provides metrics to HPA; HPA polls on its own interval. That is event-driven in the sense of event sources as input, but still technically interval-based control loops.
KEDA also has the concept of external push scalers: a scaler can push activation status to KEDA through a gRPC mechanism (StreamIsActive). The ScaledObject spec explicitly states this is an exception to normal activationTarget logic: external push scalers can push activation status directly, regardless of metric value/activation target.
This push variant is relevant where polling latency is too expensive (for example when you want faster 0→1 transitions than the polling interval allows). In practice, polling still remains part of the system: push scalers are still integrated with HPA’s periodic control loop.
Queue length, lag, backlog, and throughput: signals and semantics
Many KEDA scenarios revolve around queue and stream systems. The main metrics that appear there are:
Queue length / backlog Number of items still waiting to be processed. This is often the most direct work signal: it represents unfinished work. KEDA docs explicitly describe this pattern: with no pending messages a deployment can scale to 0; on message arrival KEDA activates it; with more messages KEDA feeds metrics to HPA for scale-out.
Consumer lag In streams (log-based systems), lag measures delay: consumer position trails producer position. Semantically this is backlog, but with nuances: lag can be stale due to rebalances and offsets, and lag does not directly express processing time per event.
Throughput Work per time or events per second. This can be more stable than queue length over short windows, but less suitable if your goal is to clear backlog within a time budget.
A useful lens for relating queue length/backlog to performance is Little’s Law: in steady state, (L = \lambda W), where (L) is average items in the system, (\lambda) is arrival rate, and (W) is average time in system. Queueing theory emphasizes this under very general conditions and links “how much work is waiting” to “how fast you process it” through flow time.
Why this matters for autoscaling: if you have an SLO on maximum waiting time or “backlog must clear within X minutes”, you can derive a backlog target instead of relying on CPU. This is not magic: service-time variance, burstiness, and batching make it hard, but conceptually it aligns better with the real domain problem than CPU%.
Activation versus scaling: two regimes
KEDA formalizes a split that is often implicit in autoscaling setups:
- Activation phase (0↔1): KEDA operator decides whether workload should move to/from 0, based on IsActive with possible separate activation thresholds.
- Scaling phase (1↔N): once there is at least one replica, KEDA leaves regular scaling decisions to HPA based on exposed metrics.
This split also explains odd behaviors when activationThreshold and threshold are chosen inconsistently. OpenShift/KEDA derivatives explicitly document that activationThreshold can effectively take priority: if scaling threshold is low but activation threshold high, KEDA can keep a workload at 0 while HPA would otherwise scale up.
Stability: flapping, hysteresis, and stabilization windows
Once you scale on external metrics (especially noisy or delayed ones), control-loop stability quickly becomes a central issue. Kubernetes explicitly documents thrashing/flapping and provides stabilization windows to dampen replica oscillation when metrics fluctuate.
KEDA adds its own timing knobs for 0→1 and 1→0 (pollingInterval, cooldownPeriod, activationThreshold), but classic damping for 1↔N still mainly lives in HPA behavior (stabilization windows, scaling policies).
Workload types where KEDA often fits well
Queue-based workers and work queues
The canonical KEDA workload is a worker deployment consuming from a queue. This is exactly where CPU is an indirect signal and queue depth/backlog is a direct one. KEDA docs describe this model explicitly: no messages means scale to 0, arrivals trigger activation, and growing queues feed HPA for scale-out.
Why this works well:
- The event source is usually the single source of truth for “how much work exists”.
- Replicas are often horizontally scalable: more consumers means more parallelism, as long as downstream systems can handle it.
- The pattern combines well with scale-to-zero because idle workers are often pure cost.
Important nuance: queue-based scaling assumes that “more consumers” is actually useful. With locked-message semantics, ordering constraints, or shared downstream bottlenecks, additional parallelism can mainly create contention. In those cases you need backpressure/flow control or a different scaling signal (for example downstream saturation) rather than pure queue depth.
Background processing and asynchronous tasks
Asynchronous background tasks (mail rendering, image/video processing, indexing, ETL-style pipelines) often have bursty load: periods of no work followed by batches. Scale-to-zero is attractive here, but only if cold-start latency is acceptable and task processing is idempotent under retries and duplicates. KEDA documentation explicitly positions scale-to-zero for this no-pending-work pattern.
Event streams and consumer groups
For stream-based processing (log-based messaging), the most relevant metric is often consumer lag or pending entries count. KEDA’s scaler catalog includes stream-oriented triggers that explicitly support pending-entries style metrics.
KEDA adds value here by providing a standard path to project stream-lag metrics into external.metrics.k8s.io, including 0→1 activation. Compared to hand-rolled HPA + custom adapter setups, this usually means less cluster glue code.
HTTP services with external load signals
For classic request/response services, “scale on HTTP requests” is conceptually more direct than “scale on CPU”, but Kubernetes has no native requests-per-second metric; you need to measure it through observability/proxy layers and expose it as custom/external metrics.
KEDA provides two paths:
- scaling on monitoring system metrics (for example Prometheus queries through the Prometheus scaler)
- the HTTP add-on model: an interceptor/proxy accepts requests, buffers them while backend is at 0, and provides pending-request metrics to KEDA via an external scaler; the operator creates required resources. The KEDA HTTP add-on flow explicitly describes this.
Useful, but with a fundamental trade-off: you move the request boundary into a buffering component that must handle timeouts, retries, and backpressure correctly. Architecturally valid, but not free.
Scheduled and bursty workloads
KEDA supports time-window scaling through the Cron scaler: you define a time range where workload should run at a desired replica count. KEDA also explicitly states what Cron scaler does not do: it is not meant to treat recurring schedule events as workload signals; it sets replicas in a window, but it is not an event scheduler.
This is practical for predictable peak windows (office hours, batch windows), or for warm capacity (for example proactively keeping 1 replica during a window to avoid cold starts).
Where KEDA is less suitable and how it compares to traditional HPA
When KEDA is not a good fit
KEDA is not universal. Four categories often disappoint in practice unless specific mitigations exist:
Stateful workloads KEDA can scale StatefulSets (as docs state), but “can” is not the same as “should”. Stateful systems often have constraints: shard ownership, data locality, warm caches, replication, compaction. An external signal such as queue depth says little about the cost of scale-out (rebalancing, data warmup), and may lead to thrashing or unpredictable latency.
Latency-critical workloads Scale-to-zero plus on-demand activation means cold starts. KEDA formalizes 0↔1 activation, but cannot remove inherent startup time (image pull, init, warmup, networking). If SLOs require first request latency in tens of milliseconds, 0→1 on demand is often architecturally unsuitable unless you add buffering/proxying (HTTP add-on) and accept temporary queuing.
CPU-bound batch jobs without clear backpressure If workload is pure compute (for example batch rendering) and input has no clear queue/backlog metric, CPU (or an internal metric such as tasks in progress) may actually be the best signal. KEDA can still work here (for example via metrics API or Prometheus scaler), but added value over HPA with custom metrics may be smaller while introducing extra components.
Workloads with poorly defined scaling signals If “more replicas” has no predictable effect (for example due to downstream rate limits, shared DB bottlenecks, global locks), any autoscaling system is fragile. You first need a scalable architecture (backpressure, partitioning, load shedding), then autoscaling. Backpressure exists specifically to prevent system collapse through bounded queues and consumer-driven flow.
KEDA versus traditional HPA
A vendor-neutral way to view KEDA vs HPA:
HPA is the native mechanism that adjusts replica counts based on metrics. It supports resource metrics and custom/external metrics through aggregated APIs.
KEDA is an ecosystem layer on top of that HPA capability:
- KEDA provides external metrics to HPA through its metrics adapter.
- KEDA adds an explicit activation phase (0↔1) for scale-to-zero, while HPA often stays effectively at ≥1 or depends on metric availability. KEDA defines
minReplicaCountdefaulting to 0 and documents activation explicitly. - KEDA provides a scaler catalog (integrations) and a uniform CRD model instead of “HPA + separate adapter + custom metric mapping per backend”.
But there is a clear trade-off: KEDA adds an extra control-plane layer and makes cluster behavior dependent on availability of the aggregated API (external.metrics.k8s.io), which can have cluster-wide impact. KEDA troubleshooting docs show concrete failure modes around FailedDiscoveryCheck and network/proxy/service mesh issues.
KEDA, VPA, and node autoscaling
Vertical Pod Autoscaler (VPA) has a different goal: it adjusts resource requests/limits based on usage, often through components such as an admission controller (mutating webhook) that applies recommendations at pod creation. It primarily changes pod size, not replica count.
This creates an interesting tension:
- HPA/KEDA scale horizontally: more pods.
- VPA scales vertically: larger/smaller pods.
Combinations are possible, but interactions matter: if VPA raises requests, HPA/KEDA may later need fewer pods (or more capacity per pod), and node autoscaling will react differently.
Node autoscaling (for example cluster autoscaler) is another layer: it provisions nodes for unschedulable pods and consolidates nodes when pods disappear. Kubernetes documents node autoscaling as reacting to unschedulable pods and consolidating unused nodes.
Implication for KEDA: pod scaling (KEDA/HPA) and node scaling must align. If KEDA scales aggressively to zero, node autoscaling may remove nodes and increase cold-start cost. If node autoscaling is conservative (minimum node pools), scale-to-zero at pod level may yield less savings than expected.
Metrics, reliability, cost, and design principles
Metrics, thresholds, and timing
KEDA introduces many timing knobs, and those are often more important for production behavior than which scaler type you choose.
pollingInterval Default 30s. Defines how often KEDA checks triggers, and critically how often it can activate when replicas=0. A 60s polling interval means up to one extra minute of idle latency before 0→1 even starts.
HPA sync period Default 15s cluster-wide. Even if KEDA polls every 30s, HPA may request metrics more frequently. That is why caching exists.
cooldownPeriod Default 300s, and only applies to scaling to 0. KEDA spec explicitly states cooldown starts after a trigger and is only relevant for scale-to-zero (1↔N remains HPA). This is your hysteresis knob at the idle boundary: how long to stay warm after last activity.
activationThreshold Not every scaler supports it, but where it exists it is essential: it defines when a scaler is considered active (0→1). KEDA introduced this so very small activity (for example one message) does not always trigger activation.
stabilization windows and scaling policies For 1↔N, HPA remains in charge. Kubernetes stabilization windows limit flapping by evaluating recommendations over a time window, avoiding immediate scale-down followed by immediate scale-up under noisy metrics.
A useful mental model: KEDA is mostly your edge detector and metric projector; HPA is your PID-like control loop for replica counts with stabilization.
Prometheus-based triggers and external metrics
In many platforms, monitoring telemetry is the fastest way to obtain work signals. KEDA includes a Prometheus scaler where scaling is based on a Prometheus query and can use activationThreshold.
Important nuance: scaling on monitoring data means scaling on top of a pipeline with its own delays (scrape interval, aggregation, query resolution). This can be fine for minute-scale spikes, but risky for sub-second bursts. If you aggressively scale on requests per second with minute-level scraping, you may get under-scaling or oscillation. The point is not “Prometheus is bad”, but “sampling/measurement intervals are part of your control loop”.
KEDA also exposes its own scrapeable metrics: operator, webhooks, and metrics adapter offer Prometheus endpoints, and pre-built Grafana dashboards exist to visualize metrics-server and scale-target behavior. That is important for debugging and SRE operations.
Failure modes with weak metrics and external dependencies
Event-driven autoscaling is only as reliable as the weakest part of the measurement chain. In KEDA contexts, common failure modes include:
Control plane cannot reach external metrics API
If aggregated APIService external.metrics.k8s.io is unavailable, HPA sees errors and scaling may stop. KEDA documents concrete symptoms (FailedDiscoveryCheck) and points to network/proxy/service mesh causes.
Only one active external metrics server is allowed
Because external.metrics.k8s.io is registered cluster-wide through APIService, there is a single-provider constraint. KEDA explicitly states that if you use KEDA, the active server for that API should be KEDA metrics server. This can conflict with other solutions trying to expose the same API.
Metric failures and HPA behavior Kubernetes HPA documents that when multiple metrics are used, the controller computes desired replicas per metric and takes the highest. If some metrics cannot be converted (for example fetch errors) and remaining metrics suggest scale-down, scaling is skipped; scale-up can still happen if another metric suggests higher replicas. This fail-safe behavior often avoids unintended scale-down during metric errors.
Timeouts and rate limits on event sources KEDA scalers often make HTTP/gRPC calls to external systems. Docs mention default HTTP timeouts (3 seconds) and tunable timeout settings. In practice, rate limiting or temporary brownouts in an event source can destabilize your scaling loop.
False positives, false negatives, and the cost of scaling wrong
Autoscaling errors largely fall into two classes:
False positives (over-scaling) Your signal says “more work”, but extra pods do not increase throughput (downstream bottleneck) or are triggered by noisy spikes. Result: higher cost, possibly more backend load, sometimes lower stability from thrash. HPA stabilization windows can dampen this, but do not fix semantically wrong metrics.
False negatives (under-scaling) Your signal misses work (for example sampling delay, wrong query, activationThreshold too high), so scaling happens too late. Result: backlog grows, latency rises, retries pile up, and retry storms may amplify the problem. KEDA’s activation/scaling split makes this explicit: 1→N can be tuned correctly while activation is too conservative, leaving workload at 0 too long.
A pragmatic SRE rule: if workload does not degrade gracefully under under-provisioning (no backpressure, only timeouts/retries), design flow control first and only then tune aggressive autoscaling. Reactive Streams defines backpressure specifically to prevent receivers buffering unboundedly; that idea maps directly to queue/worker systems.
Cost, efficiency, and cloud impact
KEDA has two cost implications you should not underestimate:
Scale-to-zero can produce real savings Especially for intermittent workloads: nights/weekends, periodic jobs, or event-driven pipelines with low duty cycles. Cloud providers explicitly document this in KEDA contexts: scaling workloads to zero saves resources during inactivity.
But scale-to-zero introduces latency and node churn Moving from 0→1 always has startup time (image pull, init, warmup, connection establishment). With node autoscaling, 0 pods can lead to 0 nodes (depending on pool settings), making cold starts larger. Kubernetes node autoscaling explicitly reacts to unschedulable pods and consolidates unused nodes. Aggressive pod scale-to-zero plus aggressive node scale-down can therefore create significant churn.
So the core design point is: cost vs latency is not just a tuning question; it is a product/SLO decision. If first-event latency matters, teams often intentionally keep minimum warm capacity (minReplicaCount > 0 or cron windows) and accept lower cost savings.
Design principles and best practices
KEDA configuration is ultimately control theory plus distributed systems. Several principles repeatedly hold:
Choose signals that represent work, not symptoms Queue backlog, lag, and inflight work are usually better than CPU when the goal is to clear work on time. But do not choose blindly: if downstream is the bottleneck, scale on downstream saturation (for example DB pool utilization), or introduce backpressure and max concurrency limits.
Treat retries/duplicates as normal behavior Event-driven systems often deliver at-least-once behavior (retries, redelivery). Idempotency is therefore a correctness requirement, not a nice-to-have. The Idempotent Consumer pattern captures this exactly: processing the same message repeatedly must have the same outcome as processing it once.
Why this matters directly for autoscaling: scale-out increases concurrency; concurrency increases the chance that races, retries, and duplicates surface. Without idempotency, autoscaling becomes an incident generator.
Design backpressure and bounded queues explicitly If producers are faster than consumers, you need the system to regulate itself without meltdown. Reactive Streams defines backpressure to prevent receivers from buffering arbitrarily; apply that to message processing with bounded queues, consumer pull rate, or server-side throttling.
Tune conservatively, measure, and adjust per regime KEDA has separate activation and scaling regimes, so tuning should be split too:
- optimize 0→1 for acceptable first-work latency (
pollingInterval,activationThreshold) - optimize 1→N for stability and throughput (HPA behavior, targets, stabilization windows)
Plan for metric failures with fail-safe defaults Make explicit choices about behavior when metrics are unavailable. HPA behavior under metric errors (skip scale-down on errors) helps, but you still need to monitor metrics adapter and external metrics API availability. KEDA exposes metrics to monitor operator/adapter/webhook health.
Account for cluster-wide impact of external.metrics.k8s.io
Because only one active provider can serve external metrics, platform teams need governance: who owns APIService, which component serves it, and how conflicts are prevented. KEDA docs explicitly call this out in cluster-operation guidance.
KEDA’s role in modern platform engineering
KEDA fits best in platforms where:
- event-driven architectures are first-class (queues, streams, workers)
- teams need a standard interface for autoscaling on domain metrics
- the platform team is willing to run and operate an autoscaling building block (observability, upgrades, incident response)
KEDA is therefore not a silver bullet, but a control-plane building block. The biggest gain is not magically better scaling, but the ability to manage scaling intent (triggers, activation, thresholds) as Kubernetes-native declarative config, with a consistent path to project event-source signals into HPA metrics.
Summary conclusions
KEDA is architecturally compelling when your workload-pressure signal lives outside pods (queue depth, lag, backlog, pending jobs) and when scale-to-zero or event-based activation has real value. It is especially strong for queue-based workers, event streams, bursty background processing, and scenarios where you intentionally tune 0↔1 separately from 1↔N.
KEDA is less suitable when workloads are stateful with expensive rebalancing, latency-critical where cold starts cannot be masked, or when scaling signals are poorly defined (or throughput is dominated by downstream bottlenecks). In those cases, simpler is often better: HPA on strong application metrics, or even fixed capacity with explicit backpressure, can be more reliable.
What KEDA does solve: it operationalizes event-driven autoscaling as a Kubernetes-native control-plane extension, with CRDs, scalers, and a metrics adapter that HPA can consume, plus explicit scale-to-zero activation. What KEDA does not solve: fundamental workload scalability, correctness under retries/duplicates, and the SLO trade-off between cost and latency. Those remain architecture and product decisions, not YAML parameters.