Home Resume
Homeβ€Ί Blogβ€Ί AWS Architecture Series #10 β€” Graviton + Spot: Cutting Compute Cost Without Betting the Fleet…
AWS AWS Architecture Series

AWS Architecture Series #10 β€” Graviton + Spot: Cutting Compute Cost Without Betting the Fleet

One Auto Scaling group that runs Graviton and x86 across Spot and On-Demand β€” mixed instances policy, attribute-based instance type selection, price-capacity-optimized allocation, and Capacity Rebalancing.

Business Challenge

A B2B SaaS company serves its public API from 240 m5.2xlarge instances spread across three Availability Zones in us-east-1. The fleet scales between 180 and 400 instances on a weekday curve. Every instance is x86. Every instance is On-Demand. Every instance is billed at list price.

Finance flagged EC2 as the largest single line item on the bill, and the platform team already knows the textbook answer is Spot. They also already tried it. Eighteen months ago they added four instance types to the Auto Scaling group, set the Spot allocation strategy to lowest-price, and moved 70% of the fleet onto Spot. A regional demand surge reclaimed most of that capacity inside ten minutes. The group tried to backfill with On-Demand in the same instance types and could not get it. The incident review ended with an informal rule: no Spot on anything a customer touches.

That rule is the expensive part. The outage was real, but the diagnosis was wrong β€” and so was the remedy. Five specific configuration choices caused it, and every one of them is documented as an anti-pattern.

1Four instance types is not diversification

A Spot capacity pool is one instance type in one Availability Zone. Four types across three AZs is twelve pools β€” and correlated ones, since they were all from the same generation and family. AWS's stated rule of thumb is to be flexible across at least ten instance types per workload.

Fix

Describe the shape of the compute instead of naming instances, so the group draws from dozens of pools.

2lowest-price is the highest-risk strategy

AWS does not hedge on this one: "We don't recommend the lowest-price strategy because it has the highest risk of interruption for your Spot Instances." The cheapest pool is frequently the thinnest pool, so the group deliberately concentrated itself in the capacity most likely to be reclaimed.

Fix

Use price-capacity-optimized, which weighs available capacity and price together.

3There was no floor

70% Spot with no guaranteed On-Demand meant a bad enough Spot event could take the serving tier below the minimum needed to carry traffic. Nothing in the configuration reserved a baseline that Spot conditions could not touch.

Fix

Set OnDemandBaseCapacity to the instance count that must exist regardless of Spot availability.

4Replacement was reactive

Without Capacity Rebalancing, Auto Scaling does not replace a Spot instance until after the Spot service has interrupted it and its health check has failed. The group spent the surge permanently behind the interruptions rather than ahead of them.

Fix

Enable Capacity Rebalancing to act on the rebalance recommendation, which arrives ahead of the two-minute notice.

5Failing over to On-Demand made it worse

This is the counterintuitive one. AWS explicitly discourages failing over to On-Demand to handle interruptions, because "failing over to On-Demand Instances can inadvertently drive interruptions for your other Spot Instances" β€” and once a type-and-AZ combination is under pressure on Spot, On-Demand in that same combination is often unavailable too. The backfill attempt competed with the capacity it was trying to rescue.

Fix

Diversify wide enough that no single pool matters, rather than building an escape hatch that amplifies the failure.

Architecture

The target is one Auto Scaling group that runs two CPU architectures across two purchase models. Graviton instances carry the fleet when they are available, because their discount compounds with the Spot discount. x86 instances exist mainly to widen the pool count. A fixed slice of On-Demand underneath both guarantees the service survives a total Spot withdrawal.

Diagram showing two launch templates, one arm64 and one x86_64, feeding a single mixed instances policy that contains two InstanceRequirements blocks. The resulting fleet is Spot Graviton capacity, Spot x86 capacity, and an On-Demand base, with Capacity Rebalancing handling at-risk instances.
One Auto Scaling group, two launch templates, two architectures. The per-override launch template is what allows arm64 and x86 instance types to coexist in a single group, because each needs a different AMI.

The mechanism that makes this possible

A Graviton instance needs an arm64 AMI and an x86 instance needs an x86_64 AMI, so a single launch template cannot serve both. The feature that resolves this is easy to miss: each entry in the mixed instances policy's Overrides array can carry its own LaunchTemplateSpecification. The group-level launch template is the default, and any override that names its own template uses that one instead.

Combine that with attribute-based instance type selection and you get two blocks β€” one selecting Graviton by CPU manufacturer, one selecting Intel and AMD β€” each bound to the correct AMI:

config.json
{
  "AutoScalingGroupName": "api-tier",
  "MixedInstancesPolicy": {
    "LaunchTemplate": {
      "LaunchTemplateSpecification": {
        "LaunchTemplateName": "api-tier-arm64",
        "Version": "$Latest"
      },
      "Overrides": [
        {
          "InstanceRequirements": {
            "VCpuCount":  { "Min": 4, "Max": 16 },
            "MemoryMiB":  { "Min": 8192 },
            "CpuManufacturers": ["amazon-web-services"],
            "BaselinePerformanceFactors": {
              "Cpu": { "References": [{ "InstanceFamily": "c6g" }] }
            }
          }
        },
        {
          "InstanceRequirements": {
            "VCpuCount":  { "Min": 4, "Max": 16 },
            "MemoryMiB":  { "Min": 8192 },
            "CpuManufacturers": ["intel", "amd"],
            "BaselinePerformanceFactors": {
              "Cpu": { "References": [{ "InstanceFamily": "c6i" }] }
            }
          },
          "LaunchTemplateSpecification": {
            "LaunchTemplateName": "api-tier-x86",
            "Version": "$Latest"
          }
        }
      ]
    },
    "InstancesDistribution": {
      "OnDemandBaseCapacity": 60,
      "OnDemandPercentageAboveBaseCapacity": 20,
      "SpotAllocationStrategy": "price-capacity-optimized"
    }
  },
  "CapacityRebalance": true,
  "MinSize": 180,
  "MaxSize": 400,
  "DesiredCapacity": 240,
  "VPCZoneIdentifier": "subnet-a,subnet-b,subnet-c"
}

Note what is not in that file: a list of instance types. The group is told the shape of the compute it needs β€” 4 to 16 vCPUs, at least 8 GiB of memory, CPU performance no worse than the named baseline family β€” and Auto Scaling resolves that to whatever currently matches. New instance generations are picked up automatically as AWS releases them, which is the main reason attribute-based selection beats a hand-maintained list.

How capacity gets filled

1

The On-Demand base is satisfied first

OnDemandBaseCapacity: 60 means the first 60 instances are On-Demand regardless of anything else. This is the floor the service is sized to survive on.

2

Everything above the base splits 20/80

OnDemandPercentageAboveBaseCapacity: 20 applies only above the base. At a desired capacity of 240 that is 60 On-Demand plus 36 more On-Demand and 144 Spot.

3

Spot is drawn from the deepest cheap pools

price-capacity-optimized selects pools that are simultaneously least likely to be interrupted and lowest priced, across both the arm64 and x86 requirement blocks.

4

At-risk instances are replaced before they die

Capacity Rebalancing launches a replacement on the rebalance recommendation, waits for it to pass its health check, and only then terminates the at-risk instance.

Why This Architecture Holds Up

Pool count is the actual availability lever

The original design's problem was arithmetic. Four instance types across three AZs is twelve pools. Two attribute blocks that each resolve to twenty-odd instance types across three AZs is well over a hundred, spanning two CPU architectures and three CPU vendors. A surge that drains one family no longer drains the fleet, because the group was never concentrated there. This is why the fix is diversification rather than a failover path β€” the failover path is what turned a capacity squeeze into an outage.

The rule of thumb worth memorising

AWS recommends being flexible across at least ten instance types per workload, and using attribute-based selection specifically because it keeps that flexibility current as new instance types launch.

The two discounts multiply

AWS states that Graviton-based instances cost up to 20% less than comparable x86 instances, and that Spot offers savings of up to 90% against On-Demand. These are independent reductions applied to the same instance-hour, so a Graviton Spot instance is discounted off an already lower base price. That compounding is the reason to put Graviton first in the policy rather than treating it as an equal alternative to x86.

It also changes the migration calculus. Teams often stall on Graviton because rebuilding for arm64 feels like a project with no immediate payoff. Inside a mixed instances group the payoff is immediate and the risk is bounded: if the arm64 image has a problem, those instances fail their health checks and the group fills from the x86 block instead.

Capacity Rebalancing buys minutes instead of seconds

A Spot interruption notice gives you two minutes. The rebalance recommendation is a separate, earlier signal that fires when an instance moves to elevated risk. Acting on the earlier signal is what turns a scramble into an orderly drain β€” connections finish, the load balancer deregisters the instance, and the replacement is already serving before the old one stops.

Two behaviours are worth knowing before enabling it. Auto Scaling may temporarily exceed the group's maximum size by up to 10% of desired capacity, because it launches before it terminates β€” a group pinned at its ceiling can otherwise stall rebalancing entirely. And it will only launch a replacement if that replacement has the same or better availability than the instance it replaces, so it will not knowingly trade one at-risk instance for another.

It replaces more instances, not fewer

Capacity Rebalancing does not reduce your interruption rate. It acts earlier, which means more total replacements β€” you are buying graceful continuity, not fewer events. Budget for the extra churn.

Key Architecture Decisions

Decision Choice Reasoning
Spot allocation strategy price-capacity-optimized AWS's recommended default. Weighs real-time capacity alongside price, so the group avoids thin pools instead of seeking them out.
Instance selection Attribute-based, two blocks Maximises pool count and adopts new instance generations automatically. A static list goes stale and silently narrows over time.
Architecture mix Graviton block first, x86 second Graviton's price advantage compounds with the Spot discount. x86 exists to widen the pool count and as a fallback if the arm64 image regresses.
On-Demand floor OnDemandBaseCapacity, not a percentage A percentage of a shrinking group also shrinks. An absolute base is a real floor that scaling activity cannot erode.
Weak-CPU protection BaselinePerformanceFactors Attribute matching on vCPU count alone will happily select older, slower cores. A baseline family sets a performance floor.
Interruption handling Capacity Rebalancing + lifecycle hook Acts on the rebalance recommendation rather than the two-minute notice, giving connection draining time to complete.

Constraints to design around

Several limits in this design are not obvious until you hit them:

Per-override launch templates are CLI/SDK only

The feature that lets one group run both architectures is not available in the console. Build this with the CLI, CloudFormation, or Terraform, and expect the console to render it incompletely.

Four InstanceRequirements blocks, maximum

You cannot express an unlimited taxonomy of workload shapes in one group. Two architectures plus two size envelopes already exhausts the budget.

The console pins On-Demand allocation to lowest-price

In the console the On-Demand allocation strategy is preselected as lowest price and cannot be changed. Choosing prioritized β€” which matters if you hold Reserved Instances β€” requires the API.

Lifecycle hooks must finish inside two minutes

AWS is explicit that the custom action has to complete in under two minutes. A drain routine that waits on long-lived connections will be cut off mid-flight.

Price protection is on whether you configure it or not

Attribute-based selection enables price protection by default, and the default On-Demand threshold is 20% above the price of the lowest-priced current-generation C, M, or R instance type matching your attributes. For Spot, Auto Scaling applies an optimal threshold automatically unless you override it. This is usually the behaviour you want, but it means the group may silently decline instance types you expected it to use. If you are debugging a group that will not scale out, check price protection before assuming there is no capacity β€” and note that you can effectively disable it by setting an absurdly high percentage such as 999999.

Preview before you deploy

The GetInstanceTypesFromInstanceRequirements API returns exactly which instance types your attributes resolve to, without launching anything. Run it against both blocks and count the results β€” if either returns fewer than ten types, the requirements are too narrow.

Closing Thought

The team in this scenario did not have a Spot problem. They had a concentration problem that Spot exposed. Twelve correlated capacity pools, a strategy that actively sought the thinnest of them, no guaranteed floor, and a failover path that competed with its own fleet β€” any one of those is enough to produce the outage they had, and none of them is really about Spot.

What makes the mixed instances policy worth the configuration effort is that it turns the question from "which instances do we want?" into "what does this workload actually need, and how many different ways can AWS satisfy it?" Answer the second question broadly enough and the capacity math stops being fragile. The cost reduction is almost a side effect β€” the fleet gets cheaper because it got more flexible, not the other way round.

The honest caveat: none of this makes Spot suitable for stateful or tightly coupled workloads. AWS says so directly, and no amount of diversification changes it. This pattern works for the stateless serving tier, the CI fleet, and the batch pool. It does not work for the database, and the discipline to keep that line clear is what makes the rest of it safe.

Next in this series

Messaging β€” SQS, SNS, and EventBridge, and the decision tree enterprises consistently get wrong. Three services with overlapping descriptions, three genuinely different delivery models, and a set of failure modes that only appear once you have picked the wrong one.

Official AWS Reference

Comments

How was your experience?
Your feedback helps improve this site.
PoorExcellent