Combining two scaling layers: KEDA scales pods from 0 to N based on events, Karpenter provides nodes in seconds. Queue consumer patterns, node provisioning, and cost optimization with spot plus consolidation.

In episode 15 we pressed down on costs with scale-to-zero and activation tuning — but one question remains: when the queue floods and replicas rise from 0 to 50, where do the nodes come from? Traditional HPA/Cluster Autoscaler waits for pods to be Pending before a node is added, and that can take 5-10 minutes. This episode covers Integration with Karpenter — AWS's automatic node provisioner that perfectly complements KEDA: KEDA scales pods, Karpenter scales nodes.
We'll assume you already have an EKS cluster with KEDA v2.20.2 installed. Karpenter runs as a Deployment in the karpenter namespace, listens for Pending pods, and launches instances directly through the EC2 API — without having to buy node groups upfront.
This is the most important concept in this episode — don't mix roles. There are two independent scaling axes:
| Layer | Tool | Unit | Focus |
|---|---|---|---|
| Pod scale | KEDA (via HPA) | Pod replica | Number of events/backlog |
| Node scale | Karpenter | Node/instance | CPU/memory/GPU capacity |
KEDA reads keda_scaler_metrics_value (for example SQS length), updates the HPA, and the HPA increases the Deployment's replicas. Those new replicas may be Pending because nodes are full. Karpenter sees the Pending pods, calculates the resource requirements, and launches new EC2 instances in seconds — not minutes.
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: queue-consumer
namespace: orders
spec:
scaleTargetRef:
name: queue-consumer
pollingInterval: 10
cooldownPeriod: 120
minReplicaCount: 0
maxReplicaCount: 200
triggers:
- type: aws-sqs-queue
metadata:
queueURL: https://sqs.ap-southeast-1.amazonaws.com/1234/orders
queueLength: "50"
authenticationRef:
name: sqs-authA range of 0 to 200 replicas is safe precisely because Karpenter guarantees the capacity exists. Without Karpenter, a large maxReplicaCount means waiting for the Cluster Autoscaler to add nodes — and HPA gives up if pods never become schedulable.
Karpenter works with NodePools (formerly Provisioners). A NodePool defines the instance types, zones, and allowed behaviors. Here's an example for a queue consumer workload with a mix of spot and on-demand:
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
name: orders-pool
spec:
disruption:
consolidationPolicy: WhenUnderutilized
expireAfter: 720h
limits:
cpu: "200"
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot", "on-demand"]
- key: kubernetes.io/arch
operator: In
values: ["amd64"]
nodeClassRef:
name: orders-ec2
taints:
- key: workload.keda/orders
effect: NoScheduleThe NoSchedule taint ensures only pods with the matching toleration enter this node pool — isolating batch worker resources from other workloads. karpenter.sh/capacity-type with the value ["spot", "on-demand"] lets Karpenter freely pick spot instances to save cost, then fall back to on-demand when spot isn't available.
The companion EC2 class resource for the NodePool:
apiVersion: karpenter.k8s.aws/v1beta1
kind: EC2NodeClass
metadata:
name: orders-ec2
spec:
amiFamily: AL2
role: "KarpenterNodeRole-orders"
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: orders-cluster
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: orders-cluster
userData: |
#!/bin/bash
echo "bootstrap worker node untuk queue consumer"This pattern (NodePool + EC2NodeClass) is the modern Karpenter standard — no more manual node group migration.
Here's the complete flow when the queue is full:
activationThreshold.pollingInterval; the SQS scaler reports the metric value.maxReplicaCount.cooldownPeriod expires; KEDA scales replicas down to 0; Karpenter consolidates the empty nodes.This sequence delivers end-to-end scale-up in 1-3 minutes for a normal stack (small images), versus 5-10 minutes with Cluster Autoscaler, which has to go through node group steps.
Karpenter brings two main savings that work well with KEDA:
karpenter.sh/capacity-type: ["spot", "on-demand"]; scale-to-zero workloads are ideal spot candidates because they never store state on nodes.consolidationPolicy: WhenUnderutilized makes Karpenter merge the load of several nodes onto fewer nodes, then release the empty ones — exactly when KEDA scales replicas down to zero after the queue drains.The two reinforce each other: KEDA turns off pods, Karpenter releases nodes. Without consolidation, leftover scale-up nodes stick around and the bill keeps growing.
kubectl get nodes -l karpenter.sh/nodepool=orders-pool
kubectl logs -n karpenter deploy/karpenter -f
karpenter nodes listkarpenter nodes list shows the nodes managed by Karpenter, including age, capacity, and disruption reasons — very useful for validating whether nodes really get released after scale-down.
Tip
Set a faster pollingInterval (10 seconds) and a moderate cooldownPeriod when pairing with Karpenter. Karpenter provides nodes quickly, but provisioning still takes a few dozen seconds — slow polling extends the total scale-up latency.
spec.limits, a single flooded queue can launch dozens of the most expensive instances. Cap it with limits.cpu/memory.["spot"], in a region that runs out of spot the queue never gets processed.maxReplicaCount out of fear of capacity. The whole point of KEDA + Karpenter is to remove that fear — let KEDA raise replicas and Karpenter provide nodes.karpenter.sh/do-not-disrupt.This episode unites the two autoscaling layers: KEDA scales pods from 0 to N based on events through an SQS ScaledObject, and Karpenter provides nodes in seconds via NodePool and EC2NodeClass. The spot + consolidation combination cuts costs, while the event → pod ready cycle closes the latency gap that usually bothers people.
Points you should take away:
limits in the NodePool is the first cost safety net.With the KEDA + Karpenter foundation in place, it's time to broaden your view of the ecosystem. In the next episode, 17, we discuss Advanced Scalers & Ecosystem: advanced scalers like GitHub API, Azure Storage Queue, GCP Cloud Storage, external-push, and integration with Argo Rollouts, Knative, and service mesh. See you in episode 17!