# EC2 Spot

> Spot migration candidates are running non-production EC2 instances with 30-day average CPU at or above 5%, after vetoes for production tags, DR/standby roles, bastions, stateful data services, and Windows licensing. ZopNight derives the savings fraction from real on-demand versus Spot SKU rates, abstaining whenever no live rate exists.

Source: https://zop.dev/integrations/aws/recommendations/ec2-spot
Updated: 2026-08-19

---

## The vetoes that keep a box off Spot

- resource.Status: running
- idle-abstain (folded from RC-013 merge): 30-day avg CPUUtilization \< 5% => defer to RC-003 (rightsizing) / RC-001 (idle stop). Only abstains when the CPU series is present (5% deliberately matches the RC-003 1-tier band so the rules partition cleanly).
- CPU confidence/evidence: CPU present and ≥ 5% => rec carries the windowed CPU series as MetricEvidence at Confidence=high; CPU absent (rare; CPUUtilization is [native to the AWS/EC2 CloudWatch namespace](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/viewing_metrics_with_cloudwatch.html), agent-free) => rec still fires at Confidence=medium with no CPU evidence.
- metadata: lifecycle=spot / purchase_type=Spot => skip, already Spot (source of truth)
- tag fallback: instance_lifecycle=spot (customer-applied)
- tag: any spotinst:* key => skip, managed by Spot.io Ocean / Elastigroup (already spot-optimized)
- tag: aws:autoscaling:groupName => skip (ASG-managed)
- env tag (env/environment/stage/tier = prod): hard veto ahead of pricing (item T0061): a production workload receives no Spot rec. Suitability is not inferred from name-absence; the dev/test-name veto was removed (RC-013 fold) so dev/test boxes now fire here.
- prod-NAME veto (#1906 B1): a prod-named box with no env tag (e.g. PRD-A-SHAN-38) is now vetoed via the shared isProdName helper, not only via isDevTestName.
- critical-role NAME veto (#1906 B2): a DR / backup / standby / failover / replica box is a resilience asset that must absorb production traffic on failover, so it must never run on interruptible Spot. Vetoed via matchesCriticalRole, where criticalRolePatterns = dr, drp, disaster, backup, bkp, standby, passive, failover, replica, matched at a segment boundary (a regex bounded by start/end or a non-alphanumeric separator) so dr trips DR-A-02 / x-standby-y but not drive.
- bastion / jump-host veto: a Spot reclaim (2-minute notice) tears down the SSH entry point and drops access to the whole environment behind it, so it is vetoed via isBastionOrJumpHost.
- stateful data-service veto (W4-F05): a self-managed database / Kafka / Elasticsearch / Redis box holds durable state and is typically single-replica, so a Spot reclaim risks data loss with no graceful drain, so it is vetoed via matchesStatefulWorkload (prefers the discoverer's engine/data-tier classification, falls back to a boundary-matched name token).
- OS/license veto (#1906 B4): Windows and SQL-Server instances carry per-second licensing and slow boot/license-activation, so a Spot reclaim is materially more disruptive. Read via isSpotIncompatibleOS from resource.Metadata (os_family, falling back to the raw platform string); an absent OS signal abstains (the rec may still fire) rather than vetoing on missing evidence.

## Real per-SKU spot delta, or nothing

real-rate-or-abstain (2026-06-27 directive: no advisory/$0 fallback for a cost rule). fraction = model.DeriveSpotFraction(tiers, \<negative sentinel>) returns a genuine per-SKU delta against [the Spot rate for that instance type](https://aws.amazon.com/ec2/spot/pricing/) in (0,1), or the negative sentinel on any guard failure (missing/zero/inverted/out-of-band OnDemand or Spot rate). A non-positive result is exactly "no live rate" and the rule abstains, returning nil. When a real rate is present, savings = cost × fraction, optimized = cost − savings. The cost basis arrives via EvaluationContext.Pricing; a flat fraction is never fabricated on it.

## Moving to a mixed instances policy

1. Assess workload interruption tolerance (stateless? containerised? batch?)
2. If suitable, replace the standalone instance with an Auto Scaling Group using a mixed instances policy (or an EC2 Fleet, request type=instant, for non-stateful single-replacement cases)
3. Use Spot interruption notices (2-minute warning) to drain gracefully
4. Consider multiple instance types to improve availability
5. Tag the new instances with instance_lifecycle=spot for tracking

## CPU utilization as idle gate and evidence

CPUUtilization (declared via RequiredMetrics, 30d; idle-abstain gate + evidence; read from evalCtx.MetricsByName via MetricByNameForUID)
