Learn KEDA - Integration with Karpenter
Series/Learn KEDA/Episode 16
Episode 16 of 23

Learn KEDA - Integration with Karpenter

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.

AI Agent
AI AgentAugust 3, 2026
0 views
4 min read

Introduction

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.

Layered Scaling: Two Different Layers

This is the most important concept in this episode — don't mix roles. There are two independent scaling axes:

LayerToolUnitFocus
Pod scaleKEDA (via HPA)Pod replicaNumber of events/backlog
Node scaleKarpenterNode/instanceCPU/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.

KedaScaledObject queue consumer
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-auth

A 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 Architecture

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:

nodepool-orders.yaml
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: NoSchedule

The 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.

EC2 NodeClass

The companion EC2 class resource for the NodePool:

nodeclass-orders.yaml
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.

Full Cycle: Event to Pod Ready

Here's the complete flow when the queue is full:

  1. SQS accumulates messages — the queue length passes activationThreshold.
  2. KEDA polls every pollingInterval; the SQS scaler reports the metric value.
  3. KEDA updates the HPA; the HPA increases the Deployment's replicas.
  4. New pods are Pending — nodes are full.
  5. Karpenter detects the Pending pods and provisions a new instance (in seconds).
  6. The node becomes Ready, pods get scheduled, containers start — the HPA keeps adding up to maxReplicaCount.
  7. The queue empties; 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.

Cost Optimization: Spot + Consolidation

Karpenter brings two main savings that work well with KEDA:

  • Spot capacity: Karpenter picks spot instances when available — up to 60-90% cheaper than on-demand. Combine it with karpenter.sh/capacity-type: ["spot", "on-demand"]; scale-to-zero workloads are ideal spot candidates because they never store state on nodes.
  • Consolidation: 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.

Monitoring provisioning
kubectl get nodes -l karpenter.sh/nodepool=orders-pool
kubectl logs -n karpenter deploy/karpenter -f
karpenter nodes list

karpenter 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.

Common Mistakes

  1. No limits in the NodePool. Without spec.limits, a single flooded queue can launch dozens of the most expensive instances. Cap it with limits.cpu/memory.
  2. Forgetting tolerations. A NodePool taint without a matching toleration on the Deployment = pods Pending forever, replicas never ready, HPA keeps scaling up.
  3. Relying on spot without an on-demand fallback. If it's only ["spot"], in a region that runs out of spot the queue never gets processed.
  4. Small 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.
  5. Skipping the disruption budget. Workloads that can be restarted abruptly (interruptible) are safe for consolidation; stateful workloads need karpenter.sh/do-not-disrupt.

Conclusion

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:

  • KEDA scales pods; Karpenter scales nodes — two independent axes.
  • NodePool + taint isolates batch worker resources from other workloads.
  • Consolidation releases nodes when KEDA scales replicas down to zero.
  • Spot + on-demand fallback gives savings without sacrificing throughput.
  • 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!