Cost optimization
Pricing models, right-sizing, tiering, visibility.
An AWS (Amazon Web Services) account bills you like a hotel minibar. Every item is metered on its own, nobody stops you when you reach for the peanuts, and the itemized bill turns up long after you have eaten them. Cost Optimization is one of the six pillars of the Well-Architected Framework, AWS's checklist of what good design looks like, and it is the habit of reading that meter constantly and engineering the total down. In the cloud, your architecture choices *are* your spending choices. An oversized server, a forgotten disk, a log bucket that never moves old data to cheaper storage: each one shows up on next month's invoice. Earlier lessons covered the pricing mechanics, including EC2 (Elastic Compute Cloud) purchase options, S3 (Simple Storage Service) storage classes and consolidated billing. This lesson covers the practice. See where the money goes, resize on real data, commit only to the baseline you can prove, delete the waste, and set tripwires. All of it runs from the CLI (command line interface).
Follow the money with Cost Explorer
You cannot cut a bill you cannot read, so the loop starts with Cost Explorer, the service that rolls every metered charge into one queryable dataset. From the command line it answers to aws ce. Its default measure is *unblended cost*, which is what each line item actually charged you on the day. Compare that with *amortized* cost, which takes any upfront commitment payment and smears it evenly across the month. Two things to know before your first query. The data trails reality by up to 24 hours. And while the console view is free, every Cost Explorer API (application programming interface) request costs $0.01, which is nothing for a few ad hoc questions and real money if a dashboard polls it every minute.
aws ce get-cost-and-usage \--time-period Start=2026-06-01,End=2026-07-01 \--granularity MONTHLY \--metrics UnblendedCost \--group-by Type=DIMENSION,Key=SERVICE# {# "ResultsByTime": [{# "TimePeriod": { "Start": "2026-06-01", "End": "2026-07-01" },# "Groups": [# { "Keys": ["Amazon Elastic Compute Cloud - Compute"],# "Metrics": { "UnblendedCost": { "Amount": "4312.5518", "Unit": "USD" } } },# { "Keys": ["Amazon Relational Database Service"],# "Metrics": { "UnblendedCost": { "Amount": "1874.2003", "Unit": "USD" } } },# { "Keys": ["Amazon Simple Storage Service"],# "Metrics": { "UnblendedCost": { "Amount": "912.7741", "Unit": "USD" } } },# { "Keys": ["AmazonCloudWatch"],# "Metrics": { "UnblendedCost": { "Amount": "388.1200", "Unit": "USD" } } }# ],# "Estimated": false# }]# }
The biggest line wins your attention. EC2 is over half of this bill, so that is where right-sizing starts. To pin spend on the team that caused it, group by a cost allocation tag instead: --group-by Type=TAG,Key=team. A tag only becomes a billing dimension after you *activate* it in the Billing console, and only for charges that happen after you flip that switch, so tag early. For the exam, keep the three visibility tools straight. Cost Explorer analyzes and forecasts. Budgets alerts you when a threshold is crossed. The Cost and Usage Report (set up through Data Exports these days) is the raw, most detailed feed, dropped into an S3 bucket so Athena, the query service that reads files with SQL, can dig through it.
Right-size on evidence, not on nerves
Most oversized servers get bought the way people rent moving vans. You take the biggest one because a second trip would be embarrassing, then you drive it half empty. Right-sizing means matching an instance to what it actually uses instead of to how nervous its owner felt on launch day. The measurements come from Compute Optimizer, a free service that runs machine learning over your last 14 days of CloudWatch metrics (CloudWatch is the built-in AWS monitoring service; 93 days if you pay for enhanced infrastructure metrics) and sorts every instance into OPTIMIZED, OVER_PROVISIONED, or UNDER_PROVISIONED, each with a concrete replacement to move to. It is opt-in. One command switches it on, then you wait. It needs at least 30 hours of metric data inside the lookback window, and the analysis itself can take up to 24 hours.
# One-time opt-in (run --include-member-accounts from the org management account)aws compute-optimizer update-enrollment-status --status Active# Over-provisioned instances with the top-ranked replacementaws compute-optimizer get-ec2-instance-recommendations \--query 'instanceRecommendations[?finding==`OVER_PROVISIONED`].{arn:instanceArn,current:currentInstanceType,suggested:recommendationOptions[0].instanceType,maxCpuPct:utilizationMetrics[0].value}'# [# {# "arn": "arn:aws:ec2:us-east-1:111122223333:instance/i-0f9d8e7c6b5a43210",# "current": "m5.2xlarge",# "suggested": "m6i.large",# "maxCpuPct": 9.4# }# ]
Notice the word *peaked*. That machine never once climbed above 9.4% CPU (central processing unit) use in two whole weeks. Moving it from m5.2xlarge to m6i.large cuts that line by roughly 75% and lands on a newer chip generation at the same time. One caveat before you act on any recommendation: CloudWatch cannot see memory use unless the CloudWatch agent is installed on the machine, so Compute Optimizer is blind to memory pressure by default. Install the agent first if the workload is memory-hungry. Right-sizing also spends your spare headroom, which is only safe because Auto Scaling (covered earlier) catches the spikes. Size for the normal day. Scale for the odd one.
Commit to the floor: Savings Plans
Once the boxes are the right size, go after the *rate* you pay for the ones that never switch off. A Savings Plan works like a phone contract. You promise to spend a fixed number of dollars per hour on compute for one or three years, and AWS cuts your rate by up to 72% in return. The exam wants you to tell the flavors apart. A Compute Savings Plan follows your usage wherever it goes, across instance families, across regions, and onto Fargate and Lambda (the two services that run containers and code without you managing servers), for up to 66% off. An EC2 Instance Savings Plan pins you to one instance family in one region and pays the deeper 72% for that loss of freedom. Reserved Instances, usually shortened to RIs, are the older version of the same idea and are mostly superseded, with one exception worth memorizing: a *zonal* RI is the only discount that also reserves capacity inside a specific Availability Zone. Do not guess the number. Ask Cost Explorer to work it out from your own history.
aws ce get-savings-plans-purchase-recommendation \--savings-plans-type COMPUTE_SP \--term-in-years ONE_YEAR \--payment-option NO_UPFRONT \--lookback-period-in-days THIRTY_DAYS \--query 'SavingsPlansPurchaseRecommendation.SavingsPlansPurchaseRecommendationSummary'# {# "EstimatedROI": "45.83",# "CurrencyCode": "USD",# "CurrentOnDemandSpend": "4096.11",# "EstimatedSavingsAmount": "1287.09",# "EstimatedSavingsPercentage": "31.42",# "HourlyCommitmentToPurchase": "3.85",# "EstimatedMonthlySavingsAmount": "1287.09",# "TotalRecommendationCount": "1"# }
Read that as: promise $3.85 an hour, and the exact same workload costs $1,287 less every month. NO_UPFRONT folds the promise into the monthly bill. ALL_UPFRONT pays the whole term on day one for a slightly better rate. For work that genuinely does not mind being interrupted, such as batch renders or CI (continuous integration) build fleets, Spot still beats every commitment at up to 90% off, with the two-minute reclaim warning you met in the EC2 lesson.
Hunt the waste
Some of your bill buys nothing whatsoever. It is the cloud version of a storage unit you stopped visiting three years ago. The usual suspects: EBS (Elastic Block Store) volumes sitting in the available state, meaning detached from every instance and still billed; snapshots of servers that were deleted a year ago; load balancers nobody sends traffic to; log groups set to keep everything forever. Unattached volumes are the easiest thing to audit because the state leaves no room for argument. available means *nothing is using this*.
aws ec2 describe-volumes \--filters Name=status,Values=available \--query 'Volumes[].{ID:VolumeId,GiB:Size,Type:VolumeType,Created:CreateTime}' \--output table# ---------------------------------------------------------------------# | DescribeVolumes |# +----------------------------+------+------------------------+------+# | Created | GiB | ID | Type |# +----------------------------+------+------------------------+------+# | 2025-11-02T08:14:53+00:00 | 500 | vol-0c2d1e0f9a8b76543 | gp3 |# | 2026-01-19T22:41:07+00:00 | 100 | vol-0a1b2c3d4e5f60789 | gp2 |# +----------------------------+------+------------------------+------+
That 500 GiB (gibibyte, a unit of storage) gp3 volume charges roughly $40 a month to do absolutely nothing. Before you decide it is too small to be worth a ticket, multiply it by every region and every account you run. Then stop doing this by hand. Data Lifecycle Manager expires old EBS snapshots on a schedule for you, and S3 lifecycle rules slide cold objects down the storage tiers from the S3 lesson. When the exam tells you the access pattern is *unknown*, the answer it wants is S3 Intelligent-Tiering. Trusted Advisor bundles a lot of these checks (idle instances, unattached volumes, commitments you are barely using), though the full set of cost checks needs a Business, Enterprise On-Ramp, or Enterprise support plan.
Set tripwires with Budgets
Everything so far looks backwards at money already gone. AWS Budgets looks forward and shouts before a line gets crossed, more smoke alarm than fire report. The exam nuance is the notification type. An ACTUAL alert fires after the money is spent. A FORECASTED alert fires the moment this month's trajectory says you are heading for an overrun, which is early enough to actually do something. The catch is that AWS needs roughly five weeks of usage history before it can forecast anything at all. Budgets can also fire *budget actions* when a threshold breaks, such as attaching a deny-all IAM (Identity and Access Management) policy or stopping named EC2 and RDS (Relational Database Service) instances.
aws budgets create-budget --account-id 111122223333 \--budget '{"BudgetName":"monthly-total","BudgetLimit":{"Amount":"9000","Unit":"USD"},"TimeUnit":"MONTHLY","BudgetType":"COST"}' \--notifications-with-subscribers '[{"Notification":{"NotificationType":"FORECASTED","ComparisonOperator":"GREATER_THAN","Threshold":80,"ThresholdType":"PERCENTAGE"},"Subscribers":[{"SubscriptionType":"EMAIL","Address":"[email protected]"}]}]'# (no output on success — verify:)aws budgets describe-budgets --account-id 111122223333 \--query 'Budgets[].{Name:BudgetName,Limit:BudgetLimit.Amount,Period:TimeUnit}'# [# {# "Name": "monthly-total",# "Limit": "9000",# "Period": "MONTHLY"# }# ]
Cost is the pillar where good architecture pays you back in actual dollars, and it is one of six views AWS expects you to hold at the same time. The next lesson pulls back to the Well-Architected Framework itself: the other five pillars, the design principles underneath them, and how the exam tests whether you can weigh cost against reliability, performance, security and operations when those four start pulling in different directions.
Right-sizing without measurements is guesswork in a spreadsheet. Look at CPU, memory (which needs the CloudWatch agent installed) and network traffic before you sign any Savings Plan. Delete unattached EBS volumes and stale snapshots as a routine. They are subscriptions nobody remembers starting.
Budgets and anomaly detection are tripwires, not a strategy. The strategy lives in the architecture: lifecycle tiers on storage, Graviton processors wherever your software runs on them, and non-production environments switched off on a schedule instead of humming through the weekend.
Write down the failure you actually fear, in your own words, rather than the one printed on the marketing slide. Then check that the architecture, the alarm and the runbook all name that same failure the same way.
Try this
Two commands that usually pay for themselves. Pull last month's spend grouped by service with ce get-cost-and-usage, then list every volume that is attached to nothing.
aws ce get-cost-and-usage --time-period Start=2026-06-01,End=2026-07-01 \--granularity MONTHLY --metrics UnblendedCost \--group-by Type=DIMENSION,Key=SERVICE \--query 'ResultsByTime[0].Groups[:5].{Service:Keys[0],Cost:Metrics.UnblendedCost.Amount}' --output tableaws ec2 describe-volumes --filters Name=status,Values=available \--query 'Volumes[].{Id:VolumeId,Size:Size,Type:VolumeType}' --output table
---------------------------------------------| GetCostAndUsage |+----------------------+--------------------+| Service | Cost |+----------------------+--------------------+| Amazon Elastic Compute Cloud | 842.10 || Amazon Simple Storage Service | 210.44 |+----------------------+--------------------+vol-0abc | 100 | gp3# available = unattached — candidate for delete after snapshot check
Takeaway
Work the order and it works for you: see the spend, then right-size, then commit, and delete waste the whole time. A budget alert only catches what the architecture failed to prevent.
Next: put a 15-minute cost review on the calendar every week. Start with the top three services and the unattached volumes, before anyone is allowed to argue about Savings Plans.