Skip to content

Karpenter Blueprint: Overprovision capacity in advance to increase responsiveness (EKS Auto Mode)

Note

If you're running Karpenter OSS (not EKS Auto Mode), use capacity-buffers instead. CapacityBuffers pre-provision real, Ready nodes ahead of demand using virtual placeholder pods that only exist in Karpenter's scheduling simulation, no dummy Deployment, no preemption overhead, and no manual instance-shape guesswork. This blueprint's dummy-workload pattern predates that feature and remains here only because CapacityBuffers are not yet supported on EKS Auto Mode (Auto Mode doesn't expose Karpenter feature gates).

Separately, Karpenter also supports static capacity (spec.replicas on a NodePool), which maintains a fixed node count regardless of pod demand but is excluded from cost-based consolidation entirely. See static-nodepool if that fits your use case better than overprovisioning.

Purpose

This blueprint keeps your EKS Auto Mode cluster overprovisioned so workloads are scheduled almost instantly. It deploys a dummy workload with a low PriorityClass to reserve capacity. When real workloads arrive, the dummy pods are preempted and your pods start on already-provisioned nodes.

With a flexible NodePool (a recommended practice for efficient compute), Karpenter optimizes for cost — meaning a single dummy pod would trigger a small, cheap instance that doesn't represent useful overprovision. This blueprint uses nodeAffinity to guarantee minimum instance requirements on the dummy workload, and podAntiAffinity to ensure each replica reserves a separate node. Together, the replica count controls how many right-sized nodes are kept overprovisioned. Adjust the nodeAffinity constraints to match your actual workload's instance requirements so the overprovisioned nodes are genuinely useful when preemption happens.

This pattern is useful in scenarios where provisioning latency directly impacts workload performance, for example:

  • Data pipelines that launch a burst of pods simultaneously
  • AI/ML inference endpoints that autoscale on traffic spikes, where cold-starting a large instance adds minutes of latency
  • CI/CD runners that spin up in bursts when multiple PRs merge and waiting for nodes serializes builds
  • Event-driven workloads like webhook processors or queue consumers that scale from near-zero and risk dropped events during the scale-up window
  • Scheduled batch jobs (e.g., nightly ETL, end-of-day reconciliation) where the launch time is known and you want zero provisioning delay at kickoff

Requirements

  • An EKS cluster with Auto Mode enabled, and an EKS Access Entry granting AmazonEKSAutoNodePolicy to the node IAM role used by Auto Mode.
  • This blueprint uses the default NodeClass (eks.amazonaws.com/v1) and requires no custom NodePool or NodeClass manifests.

If you're using the Terraform template under cluster/automode/ in this repo, the cluster, node IAM role, and Access Entry are all created for you — you can skip the manual Access Entry step below.

If you are not using the cluster/automode/ Terraform template, configure the Access Entry manually:

aws eks create-access-entry \
  --cluster-name $CLUSTER_NAME \
  --principal-arn <node-role-arn> \
  --type EC2

aws eks associate-access-policy \
  --cluster-name $CLUSTER_NAME \
  --principal-arn <node-role-arn> \
  --policy-arn arn:aws:eks::aws:cluster-access-policy/AmazonEKSAutoNodePolicy \
  --access-scope type=cluster

Deploy

Let's start by deploying the "dummy" workload:

kubectl apply -f dummy-workload.yaml

After waiting for approximately two minutes, notice how Karpenter will provision the machine(s) needed to run the "dummy" workload:

> kubectl get nodeclaims
NAME            TYPE          ZONE         NODE                          READY   AGE
default-66hfd   m6g.xlarge    us-east-1b   ip-10-0-25-181.ec2.internal   True    7m20s
default-lgmtl   m6g.xlarge    us-east-1b   ip-10-0-29-137.ec2.internal   True    7m20s

And the "dummy" pods are now running to reserve this capacity:

> kubectl get pods                                                                                                             7s
NAME                            READY   STATUS    RESTARTS   AGE
dummy-workload-b48bcd44-f24ng   1/1     Running   0          7m8s
dummy-workload-b48bcd44-fghws   1/1     Running   0          7m8s

Results

When you deploy the actual workload, the dummy pods are evicted. So, let's deploy the following workload to test it:

kubectl apply -f workload.yaml

Notice how your new pods are running within seconds, and some "dummy" pods are "Pending":

> kubectl get pods
NAME                            READY   STATUS    RESTARTS   AGE
dummy-workload-b48bcd44-p7htz   0/1     Pending   0          13s
dummy-workload-b48bcd44-z76nf   0/1     Pending   0          13s
workload-6db87b48b4-28sxx       1/1     Running   0          74s
workload-6db87b48b4-9vmr8       1/1     Running   0          13s
workload-6db87b48b4-cs2fs       1/1     Running   0          74s
workload-6db87b48b4-dbk79       1/1     Running   0          13s
workload-6db87b48b4-gnhb6       1/1     Running   0          52s
workload-6db87b48b4-h9bfc       1/1     Running   0          33s
workload-6db87b48b4-hmkrx       1/1     Running   0          74s
workload-6db87b48b4-kpx5m       1/1     Running   0          33s
workload-6db87b48b4-n77fr       1/1     Running   0          74s
workload-6db87b48b4-nvf55       1/1     Running   0          74s
workload-6db87b48b4-tdpfh       1/1     Running   0          74s
workload-6db87b48b4-xx9kx       1/1     Running   0          74s
workload-6db87b48b4-z2g7n       1/1     Running   0          52s
workload-6db87b48b4-zcnbk       1/1     Running   0          74s

After waiting for approximately two minutes, you'll get the overprovision capacity and see all pods running:

> kubectl get nodeclaims                                                                            
NAME            TYPE          ZONE         NODE                          READY   AGE
default-66hfd   m6g.xlarge    us-east-1b   ip-10-0-25-181.ec2.internal   True    11m
default-lgmtl   m6g.xlarge    us-east-1b   ip-10-0-29-137.ec2.internal   True    11m
default-nzg92   m6g.xlarge    us-east-1c   ip-10-0-44-194.ec2.internal   True    57s
default-xrvmt   m6g.xlarge    us-east-1c   ip-10-0-34-140.ec2.internal   True    57s

The new machine is there because some "dummy" pods were pending and they exist to reserve capacity. If you think you won't need those "dummy" pods while your workload is running, you can reduce the "dummy" deployment replicas to 0, and Karpenter consolidation will kick in to remove unnecessary machines.

> kubectl scale deployment dummy-workload --replicas 0
deployment.apps/dummy-workload scaled
> kubectl get nodeclaims
NAME            TYPE         ZONE         NODE                          READY   AGE
default-nzg92   m6g.xlarge   us-east-1c   ip-10-0-44-194.ec2.internal   True    2m16s
default-xrvmt   m6g.xlarge   us-east-1c   ip-10-0-34-140.ec2.internal   True    2m16s

Cleanup

To remove all objects created, run the following commands:

kubectl delete -f .