Spot cuts compute bills by 60-90%, but AWS can take the machine back in two minutes. Here's how we run real production on it without paging anyone.
Spot instances are spare EC2 capacity that AWS rents out at a steep discount, usually 60-90% off on-demand. The catch is the whole deal: AWS can reclaim the machine whenever it needs the capacity back, and you get exactly two minutes of warning before it's gone. For years that reputation kept spot pinned to "batch jobs only." We run a good chunk of our production fleet on it now, and the fleet is quieter than the on-demand version was. The difference is entirely in how you set it up.
The question isn't "is this workload important." Everything in production is important. The question is "what happens when this specific instance disappears with two minutes notice."
Safe on spot:
Not safe on spot:
The honest rule we use: if you can't answer "the workload reschedules cleanly" without a pause, it doesn't go on spot yet.
The two-minute interruption notice is the entire game. AWS publishes it two ways: the instance metadata endpoint exposes a spot/instance-action field, and there's a CloudWatch/EventBridge EC2 Spot Instance Interruption Warning event. Something in your stack has to watch for it and react.
React means three things, in order:
Two minutes is enough time for a graceful shutdown and not much else. Design for it and it's a non-event. Ignore it and every eviction becomes a spike of 502s.
The single biggest lever on interruption frequency is diversification, and most teams get it wrong by asking for one instance type in one AZ. That's the worst possible spot request: you're competing for the narrowest possible pool, and when that pool tightens, everything you have goes at once.
Spread across many instance types and every AZ in the region. m6i.large, m6a.large, m5.large, m5a.large, m5n.large are near-interchangeable for a stateless service. Each is a separate capacity pool. If you'll accept fifteen instance types across three AZs, AWS is drawing from forty-five pools, and a shortage in one barely registers.
Then set the allocation strategy to capacity-optimized (or price-capacity-optimized, which we prefer). Do not use the old lowest-price strategy for anything you care about. Lowest-price chases the cheapest pool, which is cheap precisely because it's about to run dry, so it hands you the highest interruption rate. Capacity-optimized places instances in the deepest pools, trading a few cents for far fewer evictions. The few cents are worth it every time.
This is where we run most of our spot capacity, because Kubernetes already assumes nodes come and go. A few pieces have to be in place.
Node provisioning. We use Karpenter. It watches for unschedulable pods and launches the cheapest node that fits, and it's genuinely good at spot diversification if you give it room. Cluster Autoscaler works too, but Karpenter's flexibility across instance types maps naturally onto how spot wants to be used.
The termination handler. You need something that catches the interruption notice and cordons plus drains the node before AWS pulls it. Karpenter handles interruptions natively when you wire up the SQS/EventBridge queue; on Cluster Autoscaler setups, run the AWS Node Termination Handler as a DaemonSet.
PodDisruptionBudgets. These stop a drain from taking down too many replicas of one service at once. Without a PDB, a wave of evictions plus a rolling deploy can briefly leave you with zero healthy pods.
A Karpenter NodePool that captures the shape:
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: spot-general
spec:
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot"]
- key: kubernetes.io/arch
operator: In
values: ["amd64"]
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["m", "c", "r"]
- key: karpenter.k8s.aws/instance-generation
operator: Gt
values: ["5"]
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 1m
limits:
cpu: "1000"
And a PodDisruptionBudget so drains stay polite:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api-pdb
spec:
minAvailable: 75%
selector:
matchLabels:
app: api
The wide instance-category and Gt: "5" generation filter are doing the diversification work. Don't pin instance types by hand here; let Karpenter draw from the whole menu.
Pure spot is a bad bet for the baseline you can never drop below. Run a floor of on-demand or reserved capacity that keeps the service alive if spot dries up entirely, and put the elastic layer on top of it on spot. In Karpenter, a second NodePool for on-demand with a higher weight, plus pod topology spread and a PDB, gets you there. We land around 70-80% spot on stateless tiers and sleep fine.
This pairs with the rest of our cloud cost optimization work; spot is the highest-leverage single move on the compute line, but only after the diversification and draining are real.
Put every stateless service and every Kubernetes worker on spot, diversified across a dozen-plus instance types with price-capacity-optimized, behind a proper termination handler and PDBs, on an on-demand floor you can't fall through. Keep stateful primaries off it. Do that and spot stops being a gamble and starts being the default, with the on-demand bill as the thing you have to justify.
Get the latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
One bundles your whole toolchain, the other lets you build anything from parts. The right pick depends on how much glue you want to own.
GitHub Actions OIDC gets all the attention, but GitLab, Buildkite, and CircleCI issue the same signed tokens. Here's how to trust them without opening a hole.
Explore more articles in this category
The observability market is huge and the pricing is a minefield. This is the map to the tools that matter, what each is best at, and how to avoid a runaway bill.
Cloud bills grow quietly until someone asks why. This is the map for cutting spend without cutting reliability: where the money actually goes, the levers that work, and the tools worth paying for.
Datadog bills climb quietly until finance forwards the invoice. Here's the playbook we run to cut spend hard while keeping every signal that matters.
Evergreen posts worth revisiting.