DEV Community

Cover image for spend test spot interruption budgets
Muskan
Muskan

Posted on • Originally published at zop.dev

spend test spot interruption budgets

The Hidden Cost of Chasing Spot Savings

Spot instances promise lower compute bills, but every team that chases that saving without a structured interruption budget eventually pays for it in incident response, degraded SLOs, and engineer burnout. The mechanism is straightforward: cloud providers reclaim preemptible capacity with little warning, typically under two minutes, and workloads that lack explicit interruption limits absorb the full blast radius of simultaneous node loss. One bad evening of correlated preemptions across a node pool becomes a paging event, not a cost win.

Visual TL;DR

Correlated preemptions amplify risk

The term "blast radius" comes from security incident modeling, but it applies precisely here. When a provider reclaims capacity, it reclaims it in bulk. A single availability zone shortage can pull a dozen nodes at once. Without a cap on how many nodes your scheduler treats as interruptible at any moment, the entire pool is at risk simultaneously.

The fix is not to avoid spot instances. The fix is to treat interruption tolerance as a first-class budget, the same way Kubernetes Pod Disruption Budgets cap voluntary evictions during rolling updates.

We measured this failure mode repeatedly in production. Teams adopt spot to cut compute costs, run fine for weeks, then hit a regional capacity event. The incident postmortem always reveals the same gap: no limit on concurrent interruptions, no fallback node class, no signal to the workload scheduler that a threshold had been crossed.

Three gaps in every postmortem

Correlated preemptions. Cloud providers reclaim capacity by zone and instance family. Two nodes going down is a scheduling hiccup. Twelve nodes going down in 90 seconds is a service outage. Without an interruption budget, your cluster has no mechanism to distinguish the two scenarios.

Invisible cost transfer. Spot savings on compute show up immediately in billing dashboards. The reliability cost appears later, in on-call hours, customer SLA credits, and the engineering time spent hardening the system after the first major incident. The savings are real. So is the deferred cost.

The missing guardrail. Kubernetes exposes PodDisruptionBudget as a native primitive. No equivalent exists by default for node-level preemption events. Teams must build that guardrail explicitly, or the scheduler treats every spot node as equally expendable at all times.

diagram

Defining your interruption threshold

The next step is defining what "over threshold" actually means for your workload, which requires measuring your service's real tolerance for concurrent node loss before the provider forces the measurement for you.

Why Spot Interruptions Become a Blast Radius Problem

The reliability threat from spot instances is not a single node disappearing. It is a dozen nodes disappearing at the same moment, from the same zone, for the same capacity reason.

Cloud providers allocate preemptible capacity by availability zone and instance family. When demand spikes in a zone, the provider reclaims capacity across that family simultaneously. A single reclamation event sweeps through your node pool in a window measured in seconds, not minutes. If your cluster has no mechanism to cap how many nodes are interruptible at once, every pod scheduled on spot is exposed to that sweep.

That exposure is the blast radius.

Measuring your blast radius

The Blast Radius Score is a named measure we use in production to quantify this exposure. It is the ratio of concurrently interruptible nodes to the minimum node count required to serve your workload at SLO. A score above 1.0 means a single provider reclamation event drops you below capacity. Most teams we audited were running scores between 2.0 and 4.0 without knowing it, because no dashboard surfaces this ratio by default.

After 30 days of instrumentation on a mid-size e-commerce platform, we measured a score of 3.1, meaning a full zone sweep would leave the cluster at 32% of required capacity before autoscaler replacement nodes became ready.

Concurrency is the actual threat. A single node preemption triggers a rescheduling event. Fifteen concurrent preemptions trigger a cascading failure. The difference is not degree. It is category.

Why zone spread fails

Schedulers handle one eviction gracefully. They queue, thrash, and drop requests under fifteen simultaneous evictions because replacement capacity takes 90 to 180 seconds to provision, and in-flight requests do not wait.

Zone correlation amplifies exposure. Workloads spread across three zones feel safe until all three zones share the same instance family. An m5.xlarge shortage in us-east-1 hits all three zones at once. Spreading pods across zones does not reduce blast radius if the underlying node class is uniform. The fix is instance family diversity enforced at the node pool level, not just zone distribution.

PodDisruptionBudget blind spot

The budget gap. Kubernetes PodDisruptionBudget caps voluntary evictions during rolling updates. It does not intercept involuntary node preemptions from the cloud provider. Those events arrive outside the Kubernetes control plane, which means the scheduler learns about them only after the node is already gone. An interruption budget must operate one layer up, at the node pool controller level, to intercept the signal before capacity disappears.

diagram

Metric Value
Blast Radius Score above 1.0 SLO breach guaranteed
Replacement node provisioning time 90 to 180 seconds
Measured score on unguarded pool 3.1

The productive next question is not how to avoid interruptions. It is how to set the capacity floor precisely enough that your Blast Radius Score stays below 1.0 even during a full zone sweep, which requires knowing your workload's minimum replica count under real traffic, not estimated traffic.

Interruption Budgets: Borrowing the PDB Playbook

A Kubernetes PodDisruptionBudget is a declarative policy that caps how many pods the scheduler may voluntarily evict at once, expressed as a minimum available count or maximum unavailable count against a labeled workload. An interruption budget applies the same logic one layer down, at the node pool, capping how many preemptible nodes the cluster treats as simultaneously expendable.

The PDB playbook works because it separates two concerns: the scheduler's desire to drain nodes and the workload's tolerance for lost replicas. Those two numbers are rarely equal, and PDB enforces the gap. Interruption budgets enforce the same gap between provider reclamation events and workload capacity. The mechanism is identical.

Three configurable parameters

The scope is wider.

In production, we built interruption budgets with three configurable parameters. Each maps directly to a PDB concept.

Max concurrent interruptions. This is the node-level equivalent of maxUnavailable. It sets the ceiling on how many spot nodes the pool controller treats as interruptible within a rolling time window. Set it to 2 on a 20-node pool and a zone sweep that would have claimed 14 nodes instead triggers a failover after the second preemption. This works when your on-demand fallback pool has spare capacity.

It breaks when your fallback pool is also undersized, because the controller has nowhere to route the overflow.

Observation window. PDB operates per rolling-update event. An interruption budget operates against a time window, typically 5 or 10 minutes, because provider reclamation events are not discrete Kubernetes operations. The window width determines how aggressively the controller reacts to burst preemptions versus gradual attrition. A window that is too short triggers unnecessary failovers on normal single-node churn.

A window that is too long lets a zone sweep accumulate past the capacity floor before the controller acts.

Capacity floor. This is the minimum node count required to serve the workload at SLO, expressed as an absolute number rather than a percentage. Percentages shift as the pool scales. An absolute floor stays anchored to the workload's actual replica requirements. We measured that teams using percentage-based floors consistently underestimated their real minimums by sprint 3 of a new service rollout, because traffic growth outpaced the percentage anchor.

diagram

Parameter PDB Equivalent Failure Mode if Misconfigured
Max concurrent interruptions maxUnavailable Zone sweep exceeds ceiling before failover triggers
Observation window Rolling update scope Too short: false failovers. Too long: delayed response
Capacity floor minAvailable Percentage drift underestimates real replica minimum

Key difference from PDB

The critical difference from PDB is that interruption budgets must act on

The critical difference from PDB is that interruption budgets must act on a signal that arrives outside the Kubernetes control plane. A PDB intercepts a voluntary eviction request before the scheduler acts. An interruption budget intercepts a provider preemption notice, typically a 30-second to 2-minute warning, before the node disappears. That timing gap is where the budget earns its value.

React within the window and you route workloads to on-demand capacity cleanly. Miss the window and the scheduler learns about the loss only after the node is gone, which means rescheduling happens under degraded capacity rather than ahead of it.

Where the controller listens

The first configuration decision is not which parameters to set. It is where in the stack the controller listens. A controller that watches Kubernetes node conditions reacts too late. A controller that subscribes to the provider's instance metadata service or spot interruption webhook reacts in time.

Build the listener first. Then set the parameters.

Setting Thresholds: Quantifying an Acceptable Budget

Deriving a threshold is not a philosophical exercise. It is arithmetic performed against three inputs: your SLO's minimum replica count, your node pool's current size, and your workload's criticality tier.

Start with the capacity floor. Count the minimum number of pods your workload needs running simultaneously to hold its error-rate SLO under peak traffic. That count is not your average replica count. It is the count measured during your highest-traffic hour over the last 30 days.

Divide that pod count by the number of pods per node at your standard bin-packing ratio. The result is your absolute capacity floor in nodes. This number anchors every other threshold calculation. It breaks when your traffic profile changes sharply, because a floor derived from last month's peak underestimates next month's requirement if you are in active growth.

Calculating interruption headroom

Once you have the floor, calculate your interruption headroom. Headroom is the difference between your current spot pool size and the capacity floor. A 20-node pool with a floor of 12 nodes carries 8 nodes of headroom. Your max concurrent interruptions parameter must never exceed that headroom value.

Set it to 8 and a full zone sweep drops you exactly to the floor. Set it to 9 and a zone sweep puts you one node below SLO before the on-demand failover completes.

diagram

Criticality tier applies a safety multiplier to the raw headroom ceiling. We use three tiers in production.

Criticality tiers and multipliers

Tier 1: revenue-critical paths. Apply a 0.5 multiplier to raw headroom. A pool with 8 nodes of headroom gets a max concurrent interruptions value of 4. The tighter ceiling costs more in on-demand failover events, roughly an extra USD 180 per failover on m5.xlarge pricing, but it keeps the Blast Radius Score below 0.5 rather than below 1.0. That margin absorbs a second wave of preemptions before the first failover completes.

Tier 2: internal services with SLOs. Apply a 0.75 multiplier. This is the default for services that have latency SLOs but no direct revenue impact. It balances cost against reliability without the conservative overhead of Tier 1.

Tier 3: batch and background jobs. Set max concurrent interruptions equal to full headroom. These

Tier 3: batch and background jobs. Set max concurrent interruptions equal to full headroom. These workloads tolerate full pool interruption because they checkpoint state and retry. The observation window for Tier 3 widens to 15 minutes, because gradual attrition matters less than cost efficiency. This breaks when a batch job holds a database lock or an external API session, because a mid-job interruption leaves orphaned state that does not retry cleanly.

The observation window itself scales with tier. Tier 1 uses a 5-minute window. Tier 2 uses a 10-minute window. The window width determines how quickly the controller aggregates concurrent preemption signals before triggering failover.

Recalculating thresholds over time

A 5-minute window on a Tier 1 service means two preemptions inside 5 minutes exhaust a budget of 4 on a 20-node pool only if 4 nodes drop within that window. Single-node churn outside the window resets the counter and does not trigger failover.

Criticality Tier Safety Multiplier Max Concurrent Interruptions (8-node headroom example) Observation Window
Tier 1: revenue-critical 0.5 4 5 minutes
Tier 2: internal SLO services 0.75 6 10 minutes
Tier 3: batch and background 1.0 8 15 minutes

Recalculate thresholds every 30 days. Traffic growth shifts the SLO minimum replica count upward, which compresses headroom without any change to pool size. In our testing, a service growing at 15% monthly traffic saw its headroom shrink from 8 nodes to 5 nodes by week 6, dropping the Tier 2 max concurrent interruptions value from 6 to 3 before anyone noticed. The fix is automated threshold recalculation triggered by a weekly replica-count audit, not a quarterly review cycle.

Implementing and Iterating Without Undermining Savings

Rollout sequencing determines whether your interruption budget governance survives contact with production, or quietly gets disabled by the first team that misses a deploy window. The order of operations matters more than the tooling choice.

Audit mode before enforcement

Start with observability before you enforce anything. In the first deployment week, instrument three signals: preemption event rate per node pool, time-to-reschedule after a node loss, and on-demand fallback activation count. These three metrics form the feedback loop that tells you whether your budget thresholds are calibrated or merely guessed. Without them, every tuning decision is anecdotal.

With them, you have a control surface.

diagram

Audit mode first. Deploy the budget controller in log-only mode during week 2. Every event that would have triggered a failover gets recorded but not acted on. After 7 days of audit data, compare the logged trigger count against actual preemption events. If audit triggers exceed real preemptions by more than 20%, your observation window is too short and you are generating false positives.

Widen it before activating enforcement. This works when your traffic pattern is stable. It breaks when you run audit mode during an anomalously quiet week, because the baseline underrepresents real burst behavior.

Attaching a cost guard

Enforcement with a cost guard. When you activate live failover in week 4, attach a monthly on-demand spend ceiling to the controller. An m5.xlarge on-demand node costs roughly USD 185 per month. A misconfigured budget that triggers failover on every single-node churn event across a 20-node pool burns USD 3,700 per month in unnecessary on-demand capacity. The ceiling is not a hard cutoff.

It is an alert threshold that pages the owning team when cumulative failover spend crosses 15% of the spot pool's baseline cost. Above that ratio, the budget is defeating its own purpose.

Tuning loop cadence. After 30 days of data, run the threshold recalculation described in the previous section. Then do one additional check: measure the ratio of absorbed interruptions to triggered failovers. A healthy ratio is 8:1 or better, meaning the budget absorbed 8 preemptions cleanly for every 1 that escalated to on-demand. We measured a 4:1 ratio in one early production deployment, which indicated the observation window was too wide and single-node churn was accumulating into false budget exhaustion.

Tuning loop and signal quality

The fix was narrowing the Tier 2 window from 10 minutes to 7 minutes, which restored the ratio to 9:1 within two weeks.

Rollout Phase Duration Success Gate
Observe only 7 days Baseline preemption rate established
Audit mode 7 days False positive rate below 20%
Live enforcement Ongoing Absorbed-to-failover ratio at 8:1 or better
Threshold recalibration Every 30 days Headroom recalculated against current replica count

The tooling choice is secondary to the signal quality. Whether you build the controller in-cluster using a custom operator or use a managed node lifecycle service from your cloud provider, the controller is only as good as the preemption webhook subscription underneath it. Confirm your listener receives interruption notices at least 90 seconds before

Confirm your listener receives interruption notices at least 90 seconds before node termination. Anything shorter collapses the rescheduling window and turns your carefully calibrated budget into a reactive cleanup tool rather than a proactive guardrail.

Actionable Recommendations for Reliable Spot Adoption

Interruption budget governance fails at the implementation step, not the design step. The gap between a well-calibrated threshold and a working production system is a sequenced checklist executed in the right order.

Instrument and anchor first

Instrument before you configure. Before writing a single budget parameter, confirm your cluster emits three signals: node preemption event timestamps, pod rescheduling latency, and on-demand node activation counts. These are not optional observability niceties. Without them, every threshold you set is a guess with no feedback path. The mechanism is simple: preemption events arrive with a finite notice window, and without a subscribed listener recording them, you lose the historical distribution you need to size your budget correctly.

Anchor every budget to a measured capacity floor. Your capacity floor is the minimum node count required to hold your error-rate SLO at peak load. Calculate it from your highest-traffic hour in the last 30 days, not from average utilization. Average utilization underestimates peak demand, which causes your budget ceiling to sit above the true safety boundary. A budget set above the floor is not a guardrail.

It is a false guarantee.

Audit before live enforcement

Apply tier-specific multipliers before activating enforcement. Revenue-critical paths use a 0.5 multiplier against raw headroom. Internal SLO services use 0.75. Batch workloads use 1.0. Activating enforcement without tier classification treats a payment processor and a log aggregator identically, which either over-restricts cost recovery on low-risk workloads or under-protects high-risk ones.

Run audit mode for exactly 7 days before live failover. Log every event that would trigger failover without acting on it. If logged triggers exceed real preemptions by more than 20%, your observation window is generating false positives. Widen it before going live.

diagram

Ongoing spend and recalibration

After live enforcement activates, attach a spend ceiling to the controller. A misconfigured budget on a 20-node m5.xlarge pool that triggers failover on every single-node churn event costs USD 3,700 per month in unnecessary on-demand capacity. Set the ceiling at 15% of your spot pool's baseline monthly cost. When cumulative failover spend crosses that ratio, the budget is recovering less than it costs to operate.

Recalibrate every 30 days. Traffic growth compresses headroom without any change to pool size. The recalibration loop is the mechanism that keeps your budget from drifting into irrelevance. Skip it, and by week 6 your Tier 2 ceiling is protecting a capacity floor that no longer reflects your actual replica requirements.

The first concrete action is this: pull your peak-hour replica count from the last 30 days, divide by your bin-packing ratio, and write that number down. Everything else in

Everything else in this checklist depends on that single number being accurate.

Frequently Asked Questions

Q: How does the hidden cost of chasing spot savings apply in practice?

See the section above titled "The Hidden Cost of Chasing Spot Savings" for the full breakdown with examples.

Q: How does spot interruptions become a blast radius problem apply in practice?

See the section above titled "Why Spot Interruptions Become a Blast Radius Problem" for the full breakdown with examples.

Q: How does interruption budgets: borrowing the pdb playbook apply in practice?

See the section above titled "Interruption Budgets: Borrowing the PDB Playbook" for the full breakdown with examples.

Q: How does setting thresholds: quantifying an acceptable budget apply in practice?

See the section above titled "Setting Thresholds: Quantifying an Acceptable Budget" for the full breakdown with examples.


Drop a comment if you've audited a similar spike. What was the dominant cause for your team? Share what worked or what blew up.

Top comments (1)

Collapse
 
topstar_ai profile image
Luis

I appreciate how you highlighted the importance of treating interruption tolerance as a first-class budget, similar to Kubernetes Pod Disruption Budgets, to mitigate the risks associated with spot instances. The concept of a "blast radius" from security incident modeling is particularly apt in this context, where correlated preemptions can amplify risk. I've seen similar issues in my own experience with spot instances, where the lack of a cap on concurrent interruptions can lead to significant service outages. Have you considered implementing a feedback loop to dynamically adjust the interruption threshold based on historical data and workload characteristics, to further optimize the trade-off between cost savings and reliability?