The short answer
Spot instances cut EKS node cost by 60–70% in practice (AWS advertises "up to 90%", real mixed-fleet averages land lower), and Karpenter is the right tool to run them because it handles the two things that make spot painful: picking instance types that rarely get interrupted, and reacting to the two-minute interruption warning automatically. The recipe is: a spot-first NodePool with wide instance diversity, Karpenter's native SQS interruption queue enabled, PodDisruptionBudgets on everything with more than one replica, and a weighted on-demand NodePool as the fallback. Do those four things and spot interruptions become invisible reschedules instead of incidents.
This post is the spot deep-dive that our Karpenter vs Cluster Autoscaler comparison didn't have room for.
What you're actually signing up for
Spot is spare EC2 capacity sold at a discount, with one condition: AWS can take it back with a two-minute warning whenever the on-demand side needs it. Each instance type in each availability zone is its own capacity pool with its own interruption rate. A c7i.2xlarge in us-east-1a and the same type in us-east-1b are different pools; one can be calm for weeks while the other churns daily.
Three numbers to internalize before you commit:
- Discount: typically 60–75% off on-demand for mainstream c/m/r types. The "90%" figure exists but only on unpopular pools.
- Interruption frequency: most broad pools sit under 5% monthly interruption rate (AWS publishes bands in the Spot Instance Advisor). With 15+ types allowed, your fleet-wide effective rate is far lower, because Karpenter avoids hot pools.
- Warning time: two minutes from the
spot/instance-interruption-warningevent to termination. Your pods must be evictable inside that window.
That last point is the whole game. Spot doesn't require exotic architecture — it requires the same graceful-shutdown hygiene that a node upgrade or a pod eviction already demands. If your services can't survive a drain, spot didn't create that problem; it just runs the drill daily.
Which workloads belong on spot
Sort your cluster with this test — "if this pod gets 2 minutes' notice and moves to another node, does anyone notice?"
Good on spot: stateless API replicas behind a Service, queue consumers, CI runners, batch/ETL jobs with checkpointing, dev/staging everything, horizontally-scaled workers managed by HPA or KEDA.
Keep on on-demand: single-replica anything, databases and stateful sets with local volumes, long-lived websocket/streaming servers that can't hand off, leader-elected singletons where failover is expensive, and jobs that run for hours with no checkpoint.
A useful starting split for a typical microservices cluster is 70% of vCPU on spot, 30% on-demand. You'll tune it, but don't start at 100% spot — the fallback tier is part of the design, not an admission of failure.
The spot NodePool
Karpenter v1 makes spot a scheduling constraint, not separate infrastructure. The critical decision is the requirements block: wide diversity is your interruption insurance. Every extra instance type is another capacity pool Karpenter can flee to.
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: spot
spec:
weight: 100 # preferred over the fallback pool
template:
metadata:
labels:
capacity-tier: spot
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot"]
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["c", "m", "r"]
- key: karpenter.k8s.aws/instance-generation
operator: Gt
values: ["5"]
- key: kubernetes.io/arch
operator: In
values: ["amd64", "arm64"] # Graviton pools are often calmer AND cheaper
- key: karpenter.k8s.aws/instance-size
operator: NotIn
values: ["metal", "48xlarge", "32xlarge"]
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
expireAfter: 720h
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidationAfter: 1m
limits:
cpu: "500"
Notes that matter:
- Don't pin to 2–3 instance types because you benchmarked them once. That recreates the fragility spot is famous for. This pool spans dozens of types across two architectures; Karpenter uses AWS's price-capacity-optimized allocation for spot, which weighs both price and how likely the pool is to be reclaimed.
- Graviton (
arm64) doubles your pool count and those pools tend to be less contested. If your images are multi-arch, this is free reliability. consolidationAfter: 1mon spot is fine — these nodes are disposable by definition, so let Karpenter repack aggressively.
The on-demand fallback pool
When every allowed spot pool is empty (it happens — usually during a regional capacity crunch), you want pods to land on on-demand automatically instead of sitting Pending:
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: on-demand-fallback
spec:
weight: 50 # lower weight = only when spot can't satisfy
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["on-demand"]
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["c", "m", "r"]
- key: karpenter.k8s.aws/instance-generation
operator: Gt
values: ["5"]
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidationAfter: 5m
limits:
cpu: "200"
Because both pools can host the same pods, Karpenter tries the higher-weight spot pool first and spills to on-demand only when spot capacity genuinely isn't there. Consolidation later moves pods back to spot when pools recover — you don't pay the fallback premium a minute longer than needed. Workloads that must never ride spot (your database proxy, the singleton scheduler) get a nodeSelector on karpenter.sh/capacity-type: on-demand and skip the game entirely.
Interruption handling: the SQS queue
This is the part most guides hand-wave. Karpenter has native interruption handling — you do not need aws-node-termination-handler on Karpenter-managed nodes. You give Karpenter an SQS queue wired to EventBridge rules for spot interruption warnings, rebalance recommendations, and scheduled maintenance events, and it does the right thing with the two-minute window: cordon the node, drain it respecting PDBs, and pre-launch replacement capacity.
The Terraform module wires this up in one flag; the queue name then goes into Karpenter's settings:
module "karpenter" {
source = "terraform-aws-modules/eks/aws//modules/karpenter"
version = "~> 20.0"
cluster_name = "prod"
enable_spot_termination = true # creates the SQS queue + EventBridge rules
}
# Helm values for the karpenter chart
settings:
clusterName: prod
interruptionQueue: Karpenter-prod # queue name from the module output
Verify it's live before you trust it:
kubectl -n karpenter logs deploy/karpenter | grep -i interruption
# expect: "watching interruption queue" on startup, and on a real event:
# "interruption initiated" ... "tainted node" ... "deleted node"
The sequence on a warning: EC2 emits the event, EventBridge drops it in the queue, Karpenter taints the node with karpenter.sh/disrupted:NoSchedule within seconds, drains pods, and — because it saw demand disappear — provisions a replacement node in parallel. On a typical cluster the replacement is Ready in 30–45 seconds, well inside the two-minute window. Your job is making sure the pods use that window well.
Making pods survive the two-minute drill
Three pieces of hygiene turn an interruption into a non-event:
1. PodDisruptionBudgets on every multi-replica service. Without a PDB, a drain can evict all replicas of a service at once if they share a node:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api
spec:
minAvailable: 2
selector:
matchLabels:
app: api
2. Spread replicas across capacity and zones so one reclaimed pool can't take the whole service down:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: api
3. Graceful shutdown that fits the window. terminationGracePeriodSeconds must be under ~110 seconds for spot pods, the app must catch SIGTERM, stop accepting work, and finish in-flight requests — and readiness probes must be honest so replacement pods only receive traffic when they're actually ready (probe guide here). A preStop sleep of 5–10 seconds papers over the endpoint-propagation race on most setups.
Measuring whether it's working
Two dashboards tell the whole story. First, interruption volume — Karpenter exports it:
# interruptions by reason over the last day
sum by (reason) (increase(karpenter_interruption_received_messages_total[24h]))
# fleet mix: how much capacity is actually on spot right now
sum by (capacity_type) (karpenter_nodes_total)
Expect single-digit interruptions per day per ~50 spot nodes with a wide pool. If one instance type dominates the interruption count, exclude it from the NodePool and let diversity absorb the loss. Second, the money: your EC2 line should drop roughly in proportion to (spot share × discount) — a cluster running 70% of vCPU on spot at a 65% discount saves about 45% of the total node bill. Track it per team with namespace cost allocation so the savings are visible where the workloads live.
Honest limits
- Spot is not for GPU capacity you depend on. GPU spot pools are shallow and heavily contested; interruption rates are the worst of any family. Fine for interruptible training, wrong for serving.
- Regional capacity crunches are real. A big retailer's flash sale or an AZ event can drain many pools at once. That's what the on-demand fallback pool is for — budget for your fallback tier actually being used a few days a month.
- Savings Plans and spot compete for the same dollars. Don't buy a compute Savings Plan sized for your whole fleet and then move 70% to spot — you'll pay commitment on capacity you no longer run. Commit to your on-demand floor only, then spot the rest. This ordering mistake is one of the most expensive in the AWS cost playbook.
- Spot prices drift. They're market prices; the discount on a specific pool can shrink. Karpenter re-evaluates at every provision, so a wide pool self-corrects — another reason not to pin types.
Bottom line
Spot on EKS in 2026 is a solved problem if you treat it as a system: wide instance diversity so there's always a calm pool, Karpenter's SQS interruption queue so the two-minute warning triggers an orderly drain instead of a surprise, PDBs and graceful shutdown so drains don't drop requests, and a weighted on-demand pool so worst-case capacity crunches degrade cost instead of availability. Do it in that order — hygiene first, spot share second. Start at 70/30, watch the interruption metrics for two weeks, and push the ratio up as the graphs stay boring. The reward is the single biggest line-item cut available on a Kubernetes bill — without buying a single reservation.