Tools · Karpenter

Karpenter

Karpenter is a node autoscaler that throws away the idea of node groups. It watches the pods your scheduler could not place, works out the cheapest machine in the entire cloud catalogue that would fit them, and launches that exact machine straight from the provider API — usually in under a minute. Then it keeps going: it continuously asks whether the same pods could run on fewer or cheaper nodes, and repacks them if they can. It solves the platform problem of “we are paying for half-empty nodes of the wrong shape, and new capacity takes five minutes to arrive.”

☺ Explain it like I’m 10

Imagine you run a removals company and people keep turning up with boxes. The old way was to own three van sizes and guess: “that pile looks like a medium, send a medium” — even when the pile only half fills it, and even when a small would have done. Karpenter is a dispatcher who measures the actual pile first, then rings the depot and rents exactly the right van for it, from any van in the country, in a minute. And every so often he looks at four half-full vans on the road and says “that all fits in two — repack it and send two back.”

🦉🦥Your hosts for this topic: Professor Owl & Sol the Sloth — Owl explains why a groupless autoscaler is a fundamentally different architecture and not just a faster one, while Sol counts every CPU-second it buys you and asks the slow question nobody asks in the design review: if it repacks the cluster at 3am, does your application actually survive it?

What Karpenter is and the problem it solves

☺ Like you’re 10: It’s a robot that looks at the pods nobody could find room for, and rents the exact right computer to hold them.

Karpenter was built at AWS, open-sourced in 2021, and donated to the Kubernetes project in 2023 — it now lives as a provider-neutral core in kubernetes-sigs/karpenter with separate cloud providers layered on top. It reached a stable v1 API in 2024. The AWS provider is by far the most mature; the Azure provider is what powers AKS Node Auto Provisioning; providers for Cluster API and several other clouds exist at varying maturity.

The node-group problem

The classic Cluster Autoscaler does not really scale nodes. It scales node groups — an AWS Auto Scaling Group, a GCP managed instance group, an Azure VM scale set — each of which is homogeneous by construction: one instance type, one AMI, one set of labels and taints. To make that model flexible, platform teams end up hand-maintaining a zoo of groups: on-demand-small, on-demand-large, spot-compute, spot-memory, gpu-a10, arm64-general, one per availability zone, times three environments. Every new workload shape means a new group and a Terraform change. And because the Cluster Autoscaler only knows “add one more of this,” a pod requesting 2 CPU and 30 GiB of memory will happily boot a node with 16 spare cores that nobody will ever use.

The latency hurts too. The Cluster Autoscaler simulates scheduling, decides which group to grow, changes the group’s desired count, then waits for the group’s own machinery to launch an instance — typically several minutes, in a loop that runs on a fixed interval. For a batch queue that spikes at 09:00, that is a long time to watch a Pending pod.

How Karpenter reframes it

Karpenter deletes the intermediate abstraction. There are no groups; there is a NodePool that expresses constraints — “anything in the c, m or r families, generation 5 or newer, amd64 or arm64, spot or on-demand, in these three zones” — and Karpenter chooses within them, per decision, based on what is actually pending. Given a batch of unschedulable pods it does the bin-packing arithmetic itself: which single instance type, or which small set of types, holds this batch most cheaply while satisfying every pod’s requests, node selectors, affinities, topology spread constraints and tolerations. Then it creates a NodeClaim, calls the provider’s fleet API directly, and the node registers and starts taking pods — commonly in 30 to 60 seconds.

The second half is what earns the money. Karpenter does not stop after provisioning; it runs a continuous disruption loop that looks for nodes to remove or replace. Empty nodes get deleted. Underutilised nodes get their pods repacked onto fewer nodes. An expensive node gets swapped for a cheaper type that still fits. This is bin-packing as an ongoing cost control rather than a one-off placement decision, and it is why the tool shows up in every FinOps conversation.

What Karpenter deliberately is not

Karpenter does not scale pods — that is the HPA, the VPA and KEDA, which decide how much pod you need while Karpenter makes sure a node exists for it. It is not a scheduler either: kube-scheduler still binds every pod, and Karpenter only reads the pending queue and changes the capacity underneath it. Nor is it a cluster lifecycle tool — clusters, control planes and networks are Cluster API and Crossplane territory. And it is cloud-specific by design: the core is generic, but you always run a provider that knows how to buy machines somewhere particular.

◆ Key idea

Karpenter turns node provisioning from “pick from a list I maintained in advance” into “solve for the cheapest fit right now.” Every capability on this page — spot-first strategies, consolidation, drift replacement, ARM adoption — falls out of that one inversion. It also explains the cost of admission: because the answer can change on every decision, your workloads must tolerate being moved.

Where Karpenter fits in a platform

☺ Like you’re 10: It lives in the basement of the platform. Nobody using the platform sees it — they just never wait for a computer.

In the reference architecture Karpenter sits in the compute substrate, alongside the Kubernetes control plane itself. It is unambiguously platform-team property: a single misconfigured NodePool can bankrupt a budget or boot a node with the wrong IAM role, so tenants should never be able to write one. What tenants do get is the effect — capacity that appears when they need it — plus, at most, the ability to select a NodePool by label or toleration. That split is a textbook example of the self-service boundary: expose the outcome, own the mechanism.

Its neighbours, and who does what

Karpenter’s manifests are ordinary Kubernetes objects delivered through GitOps like anything else — and a NodePool change being a pull request matters enormously, because that change can silently replace every node in the cluster. Prometheus scrapes Karpenter’s own metrics. OpenCost reads the karpenter.sh/capacity-type node label to price spot correctly, and the idle line OpenCost exposes is exactly what consolidation attacks. KEDA is the natural partner upstream: KEDA scales a queue consumer from zero to fifty pods, Karpenter materialises the nodes they need, and a queue that is empty overnight costs almost nothing. And PodDisruptionBudgets — the reliability primitive — are the brake that stops consolidation turning into an outage.

🦥 Sol’s slow math

“Everybody sells me on the provisioning speed. That is not where the money is. The money is in the boring second loop. A cluster that grows fast but never shrinks properly is a cluster that ratchets: every spike leaves behind another half-empty node, and by month three you are paying for the busiest hour of the quarter, permanently. Consolidation is what stops the ratchet. So when you evaluate this thing, do not time how fast a node appears — measure your request-versus-usage gap before and after, over a whole month. That number is the product.”

CNPE relevance

Be clear-eyed here: Karpenter is not on the official CNPE tool list. You will not be asked to install it or to recite its API. But the concepts it embodies are squarely in the exam’s Platform Architecture and cost material — node autoscaling, bin-packing, spot capacity, right-sizing, the relationship between pod autoscaling and node autoscaling. The likely shape of an exam encounter is conceptual: distinguishing pod scaling from node scaling, explaining what makes a pod stay Pending, or reasoning about why tight packing saves money and costs resilience. The primary lesson is Scaling & Scheduling; treat this page as the depth behind one paragraph of it, and see the tool landscape for where it sits among the named projects.

How it works — architecture, components, CRDs

☺ Like you’re 10: One program runs two loops forever: “does anything need a computer?” and “can I get rid of a computer?”

Karpenter is a single controller Deployment, conventionally in kube-system, running two or three replicas with leader election. It watches Pods, Nodes and its own custom resources, and talks to the cloud provider’s API. One chicken-and-egg problem is worth knowing: Karpenter cannot provision the node it runs on, so it normally sits on a small managed node group or on Fargate.

Pending pods scheduler found no node NodePool NodeClass the constraints 🦥 Karpenter bin-pack the batch pick type · zone · spot create NodeClaim disruption loop watch Cloud fleet API no node group in between Node joins · pods run typically 30–60 seconds Remove / replace when empty · underutilised drifted · expired spot interruption notice launch repack Brakes on disruption PDB · do-not-disrupt · budgets Karpenter never schedules a pod — it only changes what nodes exist

The three custom resources

Karpenter’s API is unusually small, which is a large part of its appeal. Two objects you write, one the controller writes for you.

Custom resourceWho writes itWhat it declares
NodePool (karpenter.sh/v1)Platform teamThe cloud-agnostic constraints: allowed instance families, sizes, architectures, zones and capacity types; labels and taints to stamp on nodes; total limits; the disruption policy, budgets and expireAfter; and a reference to a NodeClass
EC2NodeClass / AKSNodeClass (provider group)Platform teamThe cloud-specific details: AMI family and AMI selector, subnet and security-group selectors, the IAM role or instance profile, user data, disk layout, IMDS settings, and resource tags
NodeClaim (karpenter.sh/v1)KarpenterA request for one specific machine. It is the audit trail: what was asked for, which instance type was chosen, and why the launch failed if it did. Deleting one terminates the node it represents

The split matters conceptually. A NodePool is portable across clouds; a NodeClass is where the provider-specific mess is quarantined. Note also that a NodeClaim is a real object you can inspect — which makes debugging Karpenter far more pleasant than debugging an Auto Scaling Group.

Requirements, well-known labels and scheduling

A NodePool’s requirements are written in the same grammar as node affinity — a key, an operator (In, NotIn, Exists, DoesNotExist, Gt, LtGt and Lt take exactly one numeric value) and values. The keys are standard Kubernetes labels plus Karpenter’s own: kubernetes.io/arch, kubernetes.io/os, topology.kubernetes.io/zone, node.kubernetes.io/instance-type, karpenter.sh/capacity-type, and provider keys such as karpenter.k8s.aws/instance-category, instance-family, instance-generation, instance-cpu and instance-memory. Crucially, the pod’s own constraints intersect with the NodePool’s: a pod that demands arm64 will only ever be provisioned onto a node from a NodePool whose requirements permit arm64. If the intersection is empty, no node is created and the pod stays Pending — which is the single most common “why is nothing happening?” cause.

The four disruption reasons

Everything Karpenter does to an existing node has a named reason, and it is worth memorising all four because they behave differently.

ReasonTriggerBehaviourControlled by
EmptyNode has no non-daemonset podsDelete it outright after consolidateAfterconsolidationPolicy, disruption budgets
UnderutilisedPods would fit on fewer or cheaper nodesLaunch the replacement first, then drain and delete the old oneWhenEmptyOrUnderutilized, PDBs, do-not-disrupt
DriftedNode no longer matches its NodePool or NodeClass — e.g. a new AMI, a changed subnet selector, edited requirementsReplace the node so reality matches the specDisruption budgets; cannot be switched off per-node except by do-not-disrupt
ExpiredNode is older than expireAfterCordon, drain and remove it, so freshly launched capacity takes its place — forced rotation for patching and hygienespec.template.spec.expireAfter

Alongside these sits interruption handling: given a queue of provider events (on AWS, an SQS queue fed by EventBridge), Karpenter receives the spot two-minute warning, rebalance recommendations, instance state changes and scheduled maintenance notices, and proactively cordons and drains the doomed node so pods move before the machine vanishes. Without that plumbing configured, spot reclamation is an abrupt kill. Consolidation is also always replacement-first for non-empty nodes: the new node is up and ready before the old one is drained, so capacity never dips.

⚠ Drift is a mass-replacement trigger

Change one field in a NodeClass — a new AMI alias, an extra security group, a tweak to user data — and every node Karpenter manages is now drifted, and will be rolled. That is the correct behaviour and it is how you patch a fleet, but it means an innocuous-looking pull request can replace the entire cluster. Always pair NodeClass changes with disruption budgets and a maintenance window, review them like a deploy in GitOps, and check kubectl get nodeclaims after the merge.

The resources you will actually write

☺ Like you’re 10: Two files say “here’s what kinds of computers you may rent” and “here’s how to set one up”. A third bit is what app teams write to say “please don’t move me.”

A general-purpose, spot-first NodePool

This is the object you tune most. Read the requirements as a fence, not a preference: they say what Karpenter may choose, and it picks the cheapest fit inside the fence. Excluding small sizes and old generations is standard practice — tiny nodes waste capacity on daemonsets, and old generations are usually worse price-performance.

apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: general
spec:
  weight: 10                  # higher weight wins when several NodePools could serve a pod
  limits:
    cpu: "2000"               # HARD CEILING for this pool — your blast radius on cost
    memory: 8000Gi
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized   # the money setting
    consolidateAfter: 1m      # required in v1: wait this long before acting
    budgets:
      - nodes: "10%"          # never disrupt more than 10% of this pool at once
      - nodes: "0"            # ...and nothing at all during the Monday release window
        schedule: "0 9 * * mon"
        duration: 4h
        reasons: [Drifted, Underutilized]
  template:
    metadata:
      labels:
        platform.acme.dev/pool: general
    spec:
      expireAfter: 720h       # rotate every 30 days so nodes stay patched
      terminationGracePeriod: 1h   # HARD cap: after this, pods go regardless of PDBs
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: default
      requirements:
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["spot", "on-demand"]     # spot preferred; on-demand is the fallback
        - key: kubernetes.io/arch
          operator: In
          values: ["arm64", "amd64"]        # let Graviton win on price when it can
        - key: karpenter.k8s.aws/instance-category
          operator: In
          values: ["c", "m", "r"]
        - key: karpenter.k8s.aws/instance-generation
          operator: Gt
          values: ["5"]                     # nothing older than gen 6
        - key: karpenter.k8s.aws/instance-size
          operator: NotIn
          values: ["nano", "micro", "small"]   # daemonsets would eat the whole node
        - key: topology.kubernetes.io/zone
          operator: In
          values: ["eu-west-1a", "eu-west-1b", "eu-west-1c"]

The EC2NodeClass — where the cloud specifics live

Three selectors do the heavy lifting, and all three are the usual source of a failed launch. Subnets and security groups are discovered by tag, which is why the cluster’s networking must carry the discovery tag; the role is the IAM role the node assumes, and it must already be permitted to join the cluster.

apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
  name: default
spec:
  role: "KarpenterNodeRole-acme-prod"   # node IAM role; must be mapped into the cluster
  amiSelectorTerms:           # required in v1 — an alias tracks an EKS-optimised AMI family
    - alias: al2023@latest    # Pin a published version to opt out of AMI drift:
                              #   alias: al2023@<published-ami-version>
  subnetSelectorTerms:
    - tags:
        karpenter.sh/discovery: "acme-prod"   # THIS TAG MUST EXIST on the subnets
  securityGroupSelectorTerms:
    - tags:
        karpenter.sh/discovery: "acme-prod"
  blockDeviceMappings:
    - deviceName: /dev/xvda
      ebs:
        volumeSize: 100Gi
        volumeType: gp3
        encrypted: true
        deleteOnTermination: true
  metadataOptions:
    httpTokens: required      # IMDSv2 only — see the security-policy lesson
    httpPutResponseHopLimit: 1   # keeps IMDS out of reach of ordinary (non-hostNetwork) pods
  tags:
    team: platform
    cost-centre: "4471"       # so the cost tooling can attribute these instances
alias: al2023@latest means your fleet rolls when AWS ships an AMI

It is the friendliest default and the most surprising one. The moment a new AMI is published, every node is drifted and Karpenter begins replacing the cluster — unannounced, on the vendor’s schedule. For production, pin the AMI version and bump it deliberately through a pull request, so patching is a change you make rather than a change that happens to you. Combine with a budgets entry so even a deliberate roll is gradual.

What application teams write: protecting a workload

Tenants never touch a NodePool. What they can and should do is tell Karpenter what their workload tolerates. Three mechanisms cover almost everything, and the exam-adjacent point is that the workload owns its own disruption tolerance.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: checkout
spec:
  replicas: 6
  selector:
    matchLabels: { app: checkout }   # required — and it must match the template labels
  template:
    metadata:
      labels: { app: checkout }      # what the PDB and the spread constraint select on
      annotations:
        # Karpenter will not voluntarily disrupt a node running this pod.
        # Use it for singleton jobs and long migrations — NOT as a blanket default.
        karpenter.sh/do-not-disrupt: "true"
    spec:
      terminationGracePeriodSeconds: 30   # long values here stall every repack
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: ScheduleAnyway   # Karpenter honours DoNotSchedule too, but a
                                              # spread it cannot satisfy leaves pods Pending
          labelSelector:
            matchLabels: { app: checkout }
      containers:
        - name: app
          image: ghcr.io/acme/checkout:1.4.3
          resources:
            requests: { cpu: 500m, memory: 512Mi }   # Karpenter sizes nodes from THESE
---
apiVersion: policy/v1
kind: PodDisruptionBudget       # the brake: Karpenter honours this during consolidation
metadata:
  name: checkout
spec:
  minAvailable: 4               # never let a repack take us below four ready pods
  selector:
    matchLabels: { app: checkout }

One line in there deserves emphasis: Karpenter sizes nodes from resource requests, not from usage. A fleet of pods requesting four cores and using half of one will make Karpenter faithfully buy an enormous, expensive, idle cluster. Right-sizing requests is therefore a prerequisite for Karpenter saving you anything, not an optional follow-up — which is precisely the gap OpenCost exists to show you.

Day-to-day commands

☺ Like you’re 10: Mostly you ask “what did it decide, and why?” The answer is in one log stream and one list of NodeClaims.

Seeing what exists and what it chose

kubectl get nodepool
kubectl get ec2nodeclass
kubectl get nodeclaim -o wide        # the real audit trail: type, zone, capacity, node
kubectl describe nodeclaim <name>    # launch errors live in the conditions and events

# What did Karpenter actually buy, and how much of it is spot?
kubectl get nodes -L karpenter.sh/nodepool,karpenter.sh/capacity-type,node.kubernetes.io/instance-type

# Are we near the NodePool ceiling? (limits vs resources in the status)
kubectl get nodepool general -o jsonpath='{.status.resources}{"\n"}'

# Which nodes are Karpenter's at all — its nodes carry an ownership label.
kubectl get nodes -l karpenter.sh/nodepool --no-headers | wc -l

Reading the decisions

Karpenter’s controller log is unusually good: it states in plain text which pods it considered, which instance type it chose, and — when it declines — why. Ninety percent of debugging is this one command.

kubectl -n kube-system logs deploy/karpenter -f
kubectl -n kube-system logs deploy/karpenter | grep -i 'launched\|disrupt\|drift\|error'

# Why is this pod still Pending? Karpenter emits events onto the POD itself.
kubectl describe pod <pod> | sed -n '/Events/,$p'
kubectl get events --field-selector reason=FailedScheduling -A

# Force a node out (drains, respects PDBs, then terminates the instance).
kubectl delete nodeclaim <name>      # correct way — never terminate in the cloud console

# Temporarily stop all voluntary disruption on one node.
kubectl annotate node <node> karpenter.sh/do-not-disrupt=true

# Prove a NodePool works: scale something oversized and watch a node appear.
kubectl create deploy inflate --image=public.ecr.aws/eks-distro/kubernetes/pause:3.7
kubectl set resources deploy inflate --requests=cpu=1
kubectl scale deploy inflate --replicas=12 && kubectl get nodeclaim -w

Karpenter’s controller also serves a Prometheus /metrics endpoint — the port, the Service and the optional ServiceMonitor are set by its Helm chart — covering node and NodeClaim counts broken down by instance type and capacity type, scheduling and provisioning durations, voluntary disruption decisions by reason, and pod scheduling state. Alerting on “pods pending for longer than five minutes while NodeClaims are failing” is the alert that catches most real incidents; the wider pattern is in the command reference and Prometheus.

Gotchas and failure modes

☺ Like you’re 10: Here are the ways it quietly does nothing, or noisily does far too much.

Nothing is provisioned and the pod just sits there

Work the chain in order. Is the pod actually unschedulable for a capacity reason, or is it blocked by something Karpenter cannot fix — an unbound PVC, a missing image pull secret, a node affinity no NodePool can satisfy? Does any NodePool’s requirements intersect the pod’s own nodeSelector, affinity and tolerations? If your NodePool stamps a taint, the pod needs the matching toleration or it will never land there. Has the NodePool hit its limits (check .status.resources against .spec.limits)? Does the pod request more of anything than the largest permitted instance type provides — a 400 GiB memory request against a family capped at 128 GiB will simply never be satisfiable. Finally, read the events on the pod: Karpenter writes its refusal there in words. Workload triage walks the same ladder for pods that never go ready for other reasons.

Consolidation never happens

Three blockers account for nearly all of it. First, PodDisruptionBudgets that can never be satisfied — a minAvailable equal to the replica count, or a PDB on a single-replica Deployment, means no eviction is ever permitted and the node it sits on becomes immortal. Second, long terminationGracePeriodSeconds: a workload with a one-hour grace period turns every repack into an hour-long stall, and a fleet of them means consolidation effectively never converges. Third, karpenter.sh/do-not-disrupt used as a default — one team adds it to a base template, it propagates to forty deployments, and your cluster quietly stops consolidating while everyone congratulates themselves on the annotation. Diagnose by looking for the disruption decisions in the controller log; it says which pod blocked the eviction.

⚠ Over-broad requirements select surprising instances

A NodePool that permits every family and every size is technically “maximum flexibility” and practically a trap. Karpenter optimises for price-per-fit, so it will happily buy a metal instance, a burstable t family node whose CPU credits exhaust mid-load, an accelerated instance because it was momentarily cheap on spot, or a network-optimised shape your CNI has never been tested on. Fence the pool: name the categories, exclude the sizes you do not want, floor the generation, and keep GPUs and other specialised hardware in their own tainted NodePool — the pattern the AI/ML platform page relies on.

Infrastructure prerequisites that fail silently-ish

Two setup mistakes dominate. Subnet and security-group tags: the selectors match on tags, so a new subnet added without the karpenter.sh/discovery tag is invisible, and Karpenter keeps packing the zones it can see while one stays permanently empty. IAM: the controller needs permission to run and terminate instances, pass the node role and read pricing; the node role needs cluster-join permissions and must be mapped into the cluster’s auth configuration, or the instance boots, costs money, and never registers — the worst failure mode, because you pay for nodes that do nothing. Graceful spot handling additionally needs the interruption queue and its EventBridge rules. Check the troubleshooting playbook for the wider decision tree, and security & policy for why httpPutResponseHopLimit: 1 is not optional.

Churn as a reliability problem

Consolidation plus spot plus expiry means pods move — often. That is fine for a stateless web service and hostile to a long-running batch job, a stateful set with slow-attaching volumes, or anything holding a lease. The mitigations are ordinary Kubernetes: PDBs sized honestly, graceful shutdown that actually works, topology spread, and a separate conservative NodePool (on-demand only, long consolidateAfter) for workloads that genuinely cannot be moved. Aim the aggressive pool at the eighty percent that shrug.

🦥 Sol’s workshop · 30 min

On a scratch EKS cluster (or an AKS cluster with Node Auto Provisioning), install Karpenter and apply the general NodePool and default EC2NodeClass above. Deploy the inflate pause deployment, scale it to 12, and watch kubectl get nodeclaim -w while tailing the controller log — note which instance type it picked and read the sentence explaining why. Now scale inflate to 1 and time how long until the emptied nodes disappear; that is consolidateAfter plus drain. Next, add a PDB with minAvailable equal to the replica count and scale down again: consolidation stalls, and the log tells you exactly which pod blocked it. Finally, edit one harmless field in the EC2NodeClass — add a tag — and watch every node go Drifted and roll. That last thirty seconds of alarm is the most valuable part of the exercise.

Alternatives and when to choose it

☺ Like you’re 10: Other ways to get more computers exist. Some are simpler, some are dumber, one is “don’t have computers at all.”

The honest framing is that node autoscaling is a spectrum from “I manage the machines” to “I have no idea what a machine is,” and Karpenter sits nearer the far end without going all the way.

The comparison that decides it

OptionModelBest whenCosts you
KarpenterGroupless, just-in-time provisioning from constraints, plus a continuous consolidation loopVaried or bursty workloads on a supported cloud, where utilisation and cost matter and workloads tolerate reschedulingCloud-specific provider; real churn; a NodePool is a high-blast-radius object; another controller to run and upgrade
Cluster AutoscalerGrows and shrinks pre-defined homogeneous node groupsUniform workloads, on-prem or any provider Karpenter does not support, or when you need the boring, universally understood optionNode-group sprawl to maintain; minutes to provision; poor bin-packing; no cheaper-instance swaps
Managed node auto-provisioning (GKE Node Auto-Provisioning, AKS NAP)The cloud runs the autoscaler for you — AKS NAP is Karpenter under the hoodYou want the outcome without operating the controller and are happy inside one provider’s opinionsLess control over the tuning surface; provider-defined limits; you inherit their upgrade cadence
Fixed capacity, no autoscalerA static fleet sized for peakGenuinely flat load, hard regulatory placement rules, or on-prem where there is nothing to scale intoYou pay for peak all month; a real spike still leaves pods Pending
Serverless pods (Fargate, ACI virtual nodes)No nodes at all — each pod gets its own isolated computeSpiky, isolated, low-volume workloads; strong tenant isolation; nothing to patchHigher unit price at steady state; daemonsets and some volume types unsupported; no bin-packing to exploit
Cluster APIDeclarative cluster and machine lifecycleAlways, as a complement — CAPI owns the pool’s shape and Kubernetes version; Karpenter owns minute-to-minute scalingNot an autoscaler; a MachineDeployment does not right-size anything for you

A practical rule

If your cluster is on AWS or AKS, your workloads are stateless-ish, and someone has ever complained about the compute bill, Karpenter is the default answer — start with two NodePools (one aggressive spot pool, one conservative on-demand pool), fence the requirements tightly, set limits, and only then turn on WhenEmptyOrUnderutilized. If you are on-prem, on an unsupported provider, or your workloads are long-running and move badly, the Cluster Autoscaler remains a perfectly respectable choice and nobody should be embarrassed by it. And remember the ordering that platform architecture insists on: right-size your requests first. An autoscaler faithfully provisioning capacity for requests nobody uses is an expensive way to automate waste.

🎬 At the Platform Guild
🦊

Foxy: Karpenter’s installed, the NodePool is applied, and my pod has been Pending for eleven minutes. Nothing in the log. Broken?

🦉

Professor Owl: Not broken — declining. Your pod has nodeSelector: kubernetes.io/arch: arm64, and that NodePool’s requirements only permit amd64. The intersection is empty, so there is no machine it is allowed to buy. Read the events on the pod; it says so in English.

🦊

Foxy: Right. And it never warns me because…?

🦉

Professor Owl: Because a constraint that matches nothing is a perfectly valid constraint. Same lesson as every label selector you have ever debugged.

👺

Gizmo: Easy! Delete all the requirements. Let it pick from everything — maximum savings, maximum flexibility! 🤑

🦥

Sol: …and on Tuesday it buys a burstable node because spot was cheap, your CPU credits run out at lunchtime, and the p99 doubles. Cheapest-that-fits is only sensible inside a fence you drew.

🐢

Timmy: Also: put a limits block on every NodePool before it goes near production. It is the only thing standing between a runaway HPA and a five-figure afternoon.

🦥

Sol: And measure the request-versus-usage gap before you celebrate. Consolidation packs what you asked for. If everyone asks for four cores to run a health check, I will happily rent you a beautifully optimised cluster of expensive idleness.

🦆

Dot: All I actually want is for my pod to start in twenty seconds instead of five minutes. Which — once the fence is right — is exactly what happens.

Exam relevance and going further

☺ Like you’re 10: Karpenter itself isn’t on the exam list, but why it exists definitely is — and you can’t look up its website on the day.

Karpenter is not on the official CNPE tool list, so do not spend memorisation budget on its YAML the way you would for a GitOps or observability manifest. What is examinable is the surrounding concept space: node autoscaling versus pod autoscaling, why a pod is Pending, what bin-packing buys and costs, spot capacity trade-offs, and how cost decisions and reliability decisions are the same decision viewed from two sides. Karpenter is the clearest worked example of all of those, which is why it earns a page.

The documentation allowlist — read this twice

⚠ Karpenter’s own docs are not available during the exam

During the CNPE the only documentation you may open is kubernetes.io/docs, kubernetes.io/blog, task-specific documentation explicitly linked in the exam’s Quick Reference box, and locally installed man pages and /usr/share docs on the exam machine. karpenter.sh is not on that list, and neither are the AWS, Azure or Helm chart docs. If a task ever touched node autoscaling, you would be working from kubernetes.io’s own pages on the scheduler, taints, affinity and PodDisruptionBudgets — and from memory. Drill what is genuinely worth memorising on Know Cold.

⚖ CNPA vs CNPE — That allowlist is a CNPE mechanic — a performance exam can afford to let you open a narrow set of docs mid-task. CNPA offers no such allowance: it is fully closed-book multiple-choice, with zero external lookups of any kind, on kubernetes.io or otherwise. Karpenter itself won’t appear by name, but the concept-level knowledge this page drills — node autoscaling versus pod autoscaling, why bin-packing has trade-offs, why sizing follows requests — is still worth holding in your head for CNPA’s closed-book recall.

What to be able to do without notes

Explain in two sentences how Karpenter differs from the Cluster Autoscaler: groupless just-in-time provisioning of right-sized nodes chosen from the whole catalogue, plus a consolidation loop that repacks onto fewer or cheaper nodes — against fixed homogeneous node groups that only grow and shrink. Name the two objects you write (NodePool, NodeClass) and the one Karpenter writes (NodeClaim). List the four disruption reasons: empty, underutilised, drifted, expired. State the three brakes: PodDisruptionBudgets, the karpenter.sh/do-not-disrupt annotation, and NodePool disruption budgets. Say why an unsatisfiable PDB or a long terminationGracePeriodSeconds stalls consolidation. And be able to say, without hedging, that node sizing follows resource requests, not usage — the sentence that connects this page to FinOps.

Official resources for after the exam

Outside the exam the canonical sources are karpenter.sh/docs (the Concepts and Troubleshooting sections are short and unusually honest), the provider-neutral core at github.com/kubernetes-sigs/karpenter, the AWS provider at aws/karpenter-provider-aws, the Azure provider behind AKS Node Auto Provisioning at Azure/karpenter-provider-azure, and — for everything that is actually examinable — kubernetes.io on scheduling and eviction. Pair this page with Scaling & Scheduling for the full autoscaling picture, FinOps and OpenCost for the money, Reliability & Incidents for the disruption side, The Tool Landscape for where it sits among the named projects, and the glossary when a term stops making sense.

🐢 Timmy’s checkpoint

1. In two sentences, what does Karpenter do that the Cluster Autoscaler does not? 2. Name the three custom resources and say which one Karpenter writes rather than you. 3. List the four disruption reasons and say which one is triggered by editing a NodeClass. 4. Your cluster has stopped consolidating entirely. Name the three most likely blockers. 5. A pod requesting arm64 stays Pending forever although a NodePool exists. Why, most likely? 6. Karpenter decides node size from which field on the pod — and why does that make right-sizing a prerequisite rather than a follow-up? 7. During the exam, where can you look up the NodePool schema?

Check your answers
  1. It provisions right-sized nodes just-in-time directly from the cloud API — choosing instance type, size, zone and capacity type to fit the actual pending pods, with no pre-defined node groups — and it runs a continuous consolidation loop that repacks pods onto fewer or cheaper nodes and deletes the rest. The Cluster Autoscaler only grows and shrinks fixed, homogeneous node groups.
  2. NodePool (cloud-agnostic constraints, taints, limits, disruption policy) and a provider NodeClass such as EC2NodeClass (AMI, subnets, security groups, IAM role, user data) are yours. NodeClaim is written by Karpenter — one per machine it wants, and the place to look when a launch fails.
  3. Empty, Underutilized, Drifted, Expired. Editing a NodeClass (or a NodePool’s template, or a new AMI arriving via @latest) causes Drift, and will roll every affected node.
  4. (a) A PodDisruptionBudget that can never be satisfiedminAvailable equal to the replica count, or a PDB on a single replica. (b) Very long terminationGracePeriodSeconds, which stalls every drain. (c) karpenter.sh/do-not-disrupt applied broadly, often via a shared base template. The controller log names the blocking pod.
  5. The pod’s requirements and the NodePool’s requirements do not intersect — the NodePool almost certainly does not permit arm64 in its kubernetes.io/arch values. A selector that matches nothing is valid, so nothing errors; the refusal is written as an event on the pod. Other candidates: a NodePool taint with no matching toleration, or the NodePool’s limits being exhausted.
  6. From the pod’s resource requests, never from observed usage. So a fleet that over-requests makes Karpenter buy a large, expensive, idle cluster very efficiently — right-sizing requests has to come first or consolidation is just optimising waste.
  7. You can’t — karpenter.sh is not on the exam allowlist (kubernetes.io/docs, kubernetes.io/blog, task-specific Quick Reference links, and local man / /usr/share docs only). Karpenter is also not on the official CNPE tool list; what you must hold in your head is the concept of node autoscaling, drilled on Know Cold.