The NodePool is the heart of Karpenter scheduling. This episode breaks down the structure of spec.template, requirements, disruption, and weight, then how Karpenter matches pods with pools and labels the nodes it creates.

In episode 3 you successfully made Karpenter provision your first node: a NodePool named default pointing to an EC2NodeClass, the nginx workload scheduled right away, and NodeClaims appearing. Now let's dissect the component that most determines Karpenter's behavior: NodePool.
This episode breaks down the complete NodePool structure — spec.template, requirements, disruption, and weight — then explains how Karpenter matches pods with the right pool, and what labels/annotations it attaches to the nodes it creates.
The NodePool consists of two major parts: spec.template, which defines the node, and spec.disruption, which governs when a node can be replaced. There's also spec.weight for preference between pools. Let's look at a complete example:
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: general
spec:
weight: 10
template:
metadata:
labels:
team: platform
spec:
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
requirements:
- key: kubernetes.io/arch
operator: In
values: ["amd64"]
- key: karpenter.sh/capacity-type
operator: In
values: ["spot", "on-demand"]
- key: node.kubernetes.io/instance-type
operator: In
values: ["m5.large", "m6i.large", "m6a.large"]
taints:
- key: workloads
value: "true"
effect: PreferNoSchedule
disruption:
consolidationPolicy: WhenUnderutilized
expireAfter: 720hspec.template.metadata holds the labels and annotations to be attached to new nodes, for example team: platform for cost grouping. spec.template.spec contains three important things:
A requirement takes the form of key, operator, and values. Available operators: In, NotIn, Gt, Lt, and Exists. Commonly used keys:
| Key | Example values | Meaning |
|---|---|---|
| node.kubernetes.io/instance-type | m5.large, m6i.large | Filter instance type |
| karpenter.sh/capacity-type | on-demand, spot | Capacity type |
| topology.kubernetes.io/zone | ap-southeast-1a | Availability zone |
| kubernetes.io/arch | amd64, arm64 | CPU architecture |
| karpenter.sh/instance-category | general, compute | Family category |
| karpenter.sh/instance-family | m5, c6i | Instance family |
The combination of requirements determines Karpenter's "search space" for instances. The more specific, the more limited the options — the broader, the greater the chance of getting low prices via binpacking.
spec.disruption controls when a node may be terminated:
When several NodePools are equally suitable for a pod, spec.weight decides the winner. The higher the weight value (scale 0-100), the more that pool is preferred. If weights are equal, Karpenter picks the pool that comes first in order.
Scheduling in Karpenter essentially inverts the standard scheduler logic: instead of finding a node for a pod, Karpenter looks for an instance combination that can satisfy all queued pods. The process involves:
The total CPU, memory, GPU, and storage requests from all pods are summed to determine the minimum instance size. Pods with complementary requests can be bin-packed onto one node.
Labels requested by a pod via nodeSelector or nodeAffinity are matched against the labels from the NodePool's spec.template. Example: a pod with nodeSelector disktype: ssd only matches a pool that defines the disktype: ssd label.
Nodes from a NodePool can be tainted. Pods without a matching toleration will be rejected by that pool. This is the most common way to separate workloads: a Spot-only pool with a taint, where only pods prepared for interruption are allowed in.
Note
Karpenter also honors other Kubernetes constraints like node affinity, pod affinity/anti-affinity, and topology spread constraints. All of these further narrow the instance options — so don't be surprised if a combination of constraints makes Karpenter pick a larger instance.
When a node is born from a pool, Karpenter attaches labels and annotations that become an observability window:
| Label/Annotation | Type | Content |
|---|---|---|
| karpenter.sh/nodepool | label | Name of the NodePool the node came from |
| karpenter.sh/capacity-type | label | spot or on-demand |
| node.kubernetes.io/instance-type | label | Selected instance type |
| topology.kubernetes.io/zone | label | AZ where the instance was launched |
| karpenter.sh/registered | annotation | Marks the node as registered |
| karpenter.sh/do-not-disrupt | annotation | Protects a pod from consolidation |
kubectl get nodes -l karpenter.sh/nodepool=general -o wide
kubectl describe node <node-name>
kubectl get nodeclaimsThe karpenter.sh/nodepool and karpenter.sh/capacity-type labels are the ones commonly used for cost tagging and node grouping. Adding the karpenter.sh/do-not-disrupt annotation to an important pod makes Karpenter skip it during consolidation.
A common practice in production: several NodePools for different needs — one for general workloads, one Spot-only and cheaper, another for GPU. Example split:
| NodePool | capacity-type | instance-family | weight |
|---|---|---|---|
| general | on-demand | m5, m6i, m6a | 10 |
| spot | spot | c5, c6i | 20 |
| gpu | on-demand | g5, p4d | 0 |
With a higher weight, the spot pool is chosen first as long as the pods tolerate the Spot taint. This is the most frequently used cost-saving strategy.
Episode 4 dissected the NodePool — the core of Karpenter's scheduling policy. You understood the spec.template structure with requirements and taints, disruption policies like consolidationPolicy and expireAfter, weight for preference between pools, and the mechanism Karpenter uses to match pods — resource requests, labels, and taints — with the right pool. The karpenter.sh labels and annotations become an invaluable observability tool.
Key takeaways:
In episode 5 we move to the infrastructure side: EC2NodeClass. We'll cover AMI selection, security groups, subnets, instance profiles, userData, and how to combine multiple NodeClasses for different workloads. See you in the next episode!