---
title: Cost cleanup
slug: cost-cleanup
summary: A repeatable monthly sweep that finds idle and oversized cloud spend, proves it is safe to cut, and books the savings.
order: 18
updated: 2026-11-11
status: ready
---

Use this page for a monthly cost review or a savings target. Cost work is ideal for an agent: the finding is mostly read-only, and the changes are small, reviewable MRs.

## Look in this order (biggest, safest wins first)

1. **Idle resources:** unattached EBS volumes, old snapshots, idle load balancers, unused Elastic IPs, stopped instances with large volumes, NAT gateways in empty VPCs.
2. **Non-production running 24x7:** schedule dev and test to stop nights and weekends.
3. **Oversized compute and databases:** CPU under 10 percent for 30 days.
4. **Storage classes:** S3 lifecycle to infrequent access or Glacier; gp2 to gp3 volumes.
5. **Logs and metrics:** CloudWatch log groups with "never expire", high-cardinality custom metrics, verbose debug logging in production.
6. **Commitments:** Savings Plans or reserved capacity only after the above, for steady baseline usage.

## Find it (read-only)

```bash
# Spend by service, last full month
aws --profile <payer-ro> ce get-cost-and-usage \
  --time-period Start=<YYYY-MM-01>,End=<YYYY-MM-01> --granularity MONTHLY \
  --metrics UnblendedCost --group-by Type=DIMENSION,Key=SERVICE \
  --query 'ResultsByTime[0].Groups[].{svc:Keys[0],usd:Metrics.UnblendedCost.Amount}' --output table

# Unattached volumes
aws --profile <readonly-profile> --region <region> ec2 describe-volumes \
  --filters Name=status,Values=available \
  --query 'Volumes[].{id:VolumeId,gb:Size,type:VolumeType,created:CreateTime}' --output table

# Log groups with no retention
aws --profile <readonly-profile> --region <region> logs describe-log-groups \
  --query 'logGroups[?!retentionInDays].{name:logGroupName,bytes:storedBytes}' --output table

# Rightsizing suggestions
aws --profile <readonly-profile> --region <region> compute-optimizer get-ec2-instance-recommendations \
  --query 'instanceRecommendations[].{id:instanceArn,finding:finding,now:currentInstanceType,rec:recommendationOptions[0].instanceType}'
```

## Turn findings into savings

- [ ] One ticket per finding type per account, with monthly cost and the evidence.
- [ ] Estimate the monthly saving with the price list, not a guess.
- [ ] Changes as MRs; deletes follow the staged approach in [decommissioning](./aws-account-and-resource-decommissioning.md).
- [ ] Track: finding, saving estimate, date changed, saving confirmed next month.

| Finding | Account | Est. monthly saving | Status | Confirmed |
| --- | --- | --- | --- | --- |
| <n> unattached volumes | `<account-alias>` | <amount> USD | MR open | |

## Prevent it coming back

- [ ] Required tags (`owner`, `env`, `service`, `cost-center`) enforced with tag policies or IaC checks.
- [ ] Budgets with alerts per account.
- [ ] Default log retention in your IaC modules.
- [ ] Monthly report of top 10 movers by service and account.
- [ ] Cost per request for each service, so growth is not mistaken for a leak.

$$\text{cost per request} = \frac{\text{daily cost}}{\text{daily requests}}$$

For example, 400 USD a day at 20 million requests is 20 USD per million requests. If daily cost jumps to 600 USD with flat traffic, it is 30 USD per million.

## Pitfalls

- Deleting snapshots that are the only backup. Check backup policy first.
- Rightsizing a database before peak season based on quiet-month data.
- Counting savings that never showed up on the bill. Confirm next month.
