Skip to main content
CloudArq
topiccost waste
scopeaws
typehow-to
how-to guide · aws · cost

How to cut AWS cost waste

Most AWS overspend is not a big architectural mistake — it is a long tail of resources that bill by the hour whether or not anything uses them. This guide walks the practical passes that reclaim the most: find idle NAT gateways, unattached Elastic IPs and EBS, and idle load balancers; modernise gp2 to gp3; right-size EC2 and RDS from real utilisation; commit steady usage with Savings Plans; tag for cost ownership; and set an AWS Budgets alarm so new waste pages someone. Real AWS CLI, console, and Terraform steps throughout.

Updated 2026-07-20 · ~9 minute read

11
cost checks below
169
total CloudArq checks
0
resources CloudArq changes
0
credentials stored
why it matters

Why the waste is invisible until you look for it

None of this waste throws an error. An idle NAT gateway still routes; an unattached EBS volume still exists; a half-used RDS instance still answers. They simply keep accruing hourly charges no one is watching, and the Cost Explorer top line only tells you the total went up, not which resource did it. The reliable way to find it is a per-resource inventory joined to CloudWatch utilisation: enumerate what exists, ask what each thing actually did over the last one-to-four weeks, and act on the resources that did nothing. That is exactly the pass the steps below walk, and the same pass an automated cost audit runs for you.

1
step · reclaim

Find idle NAT, unattached EIP/EBS, and idle ELB

Start with the resources that bill whether or not they do work. Unattached Elastic IPs and "available" EBS volumes are pure metadata queries; an idle NAT gateway or load balancer needs a glance at CloudWatch traffic to confirm.

# Elastic IPs not associated with anything (AssociationId is null)
aws ec2 describe-addresses \
  --query 'Addresses[?AssociationId==`null`].[PublicIp,AllocationId]' --output table

# EBS volumes in the "available" state — attached to nothing, still billed
aws ec2 describe-volumes \
  --filters Name=status,Values=available \
  --query 'Volumes[].[VolumeId,Size,VolumeType]' --output table

# NAT gateway data processed over 30 days — near-zero means idle
aws cloudwatch get-metric-statistics \
  --namespace AWS/NATGateway --metric-name BytesOutToDestination \
  --dimensions Name=NatGatewayId,Value=nat-0example \
  --start-time "$(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ)" \
  --end-time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
  --period 2592000 --statistics Sum

Release unattached EIPs with aws ec2 release-address, snapshot then delete stray volumes, and delete idle NAT gateways or load balancers once you have confirmed nothing routes through them. For an idle ALB/NLB, check RequestCount / ActiveFlowCount in the AWS/ApplicationELB / AWS/NetworkELB namespaces.

2
step · modernise

Move gp2 volumes to gp3

gp3 is the newer general-purpose SSD family and AWS documents it as lower cost per GB than gp2 at the same baseline performance, with IOPS and throughput set independently of capacity. The switch is an in-place modify-volume — no snapshot, no downtime — but set an explicit baseline so you do not silently lose performance:

# List gp2 volumes to migrate
aws ec2 describe-volumes \
  --filters Name=volume-type,Values=gp2 \
  --query 'Volumes[].[VolumeId,Size]' --output table

# Convert in place; set a baseline (gp3 starts at 3000 IOPS / 125 MB/s)
aws ec2 modify-volume \
  --volume-id vol-0example \
  --volume-type gp3 --iops 3000 --throughput 125

Confirm your workload does not rely on gp2 burst-credit behaviour, then modify during a low-traffic window. The volume stays attached and readable throughout the optimisation.

3
step · right-size

Right-size EC2 and RDS from real utilisation

Right-sizing on a hunch causes incidents; right-sizing on metrics does not. AWS Compute Optimizer reads your CloudWatch history and recommends EC2, EBS, Lambda, and RDS targets — turn it on and let it observe before you resize anything:

# Enable Compute Optimizer (account-level, opt-in)
aws compute-optimizer update-enrollment-status --status Active

# Read EC2 recommendations once it has data
aws compute-optimizer get-ec2-instance-recommendations \
  --query 'instanceRecommendations[].[instanceArn,finding,currentInstanceType,recommendationOptions[0].instanceType]' \
  --output table

# Manual cross-check: 14 days of average CPU for one instance
aws cloudwatch get-metric-statistics \
  --namespace AWS/EC2 --metric-name CPUUtilization \
  --dimensions Name=InstanceId,Value=i-0example \
  --start-time "$(date -u -d '14 days ago' +%Y-%m-%dT%H:%M:%SZ)" \
  --end-time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
  --period 86400 --statistics Average Maximum

Sustained low average CPU with a low maximum means you can step down a size (or family). Change EC2 with a stop → modify-instance-attribute → start; change RDS with modify-db-instance in a maintenance window. Consider Graviton equivalents for a further efficiency gain where your workload supports arm64.

4
step · commit

Commit steady usage with Savings Plans or RIs

Once you have removed the waste, lower the rate on what remains. Savings Plans and Reserved Instances both trade a one- or three-year commitment for a discount on steady-state usage. Compute Savings Plans are the most flexible — they apply across instance family, size, region, and Fargate/Lambda. Commit only to the baseline you are confident stays up for the term, and audit existing commitments for underutilisation before buying more:

# What Savings Plans would AWS recommend for your steady usage?
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 SIXTY_DAYS

# Are the commitments you already own being fully used?
aws ce get-reservation-utilization \
  --time-period Start=2026-06-01,End=2026-07-01

Keep bursty or uncertain capacity on-demand — a commitment you do not consume is its own kind of waste. Under ~80% utilisation on an existing Reserved Instance means you over-committed and should not renew at that level.

5
step · attribute

Tag for cost ownership

Waste with no owner never gets cleaned up. Tag every resource with an owner and a cost centre, then activate those as cost-allocation tags so they appear in Cost Explorer and the billing reports. Untagged spend becomes a visible bucket you can chase down:

# Tag a resource with an owner + cost centre
aws ec2 create-tags --resources i-0example \
  --tags Key=owner,Value=platform-team Key=cost-center,Value=cc-1042

# Activate the keys as cost-allocation tags (billing-console equivalent)
aws ce update-cost-allocation-tags-status \
  --cost-allocation-tags-status TagKey=owner,Status=Active \
                                TagKey=cost-center,Status=Active

Enforce tags going forward with a tag policy or an infrastructure-as-code default, so nothing new launches without an owner. Now every dollar has a name next to it.

6
step · guardrail

Set an AWS Budgets alarm

The final guardrail catches new waste the day it appears. In the console it is Billing → Budgets → Create budget → Cost budget: set a monthly amount and alert thresholds (for example at 80% and 100% of the plan), and add the emails to notify. To keep it in version control, the Terraform below creates the same cost budget with two notifications:

resource "aws_budgets_budget" "monthly_cost" {
  name         = "monthly-cost-guardrail"
  budget_type  = "COST"
  limit_amount = "1000"      # your chosen monthly cap, in USD
  limit_unit   = "USD"
  time_unit    = "MONTHLY"

  notification {
    comparison_operator        = "GREATER_THAN"
    threshold                  = 80
    threshold_type             = "PERCENTAGE"
    notification_type          = "ACTUAL"
    subscriber_email_addresses = ["[email protected]"]
  }

  notification {
    comparison_operator        = "GREATER_THAN"
    threshold                  = 100
    threshold_type             = "PERCENTAGE"
    notification_type          = "FORECASTED"
    subscriber_email_addresses = ["[email protected]"]
  }
}

Budgets is alerting, not enforcement — it warns you the moment spend crosses a threshold; it does not cap or stop your account. Route the notification to a channel a human actually watches.

don't forget

The newer waste: idle AI and inference spend

AI workloads add a fresh category of always-on spend that the classic cost tools miss. A SageMaker endpoint bills 24/7 whether or not anyone invokes it; a Bedrock provisioned- throughput commitment bills hourly with no usage floor; a retrieval-augmented-generation knowledge base on a serverless vector store carries a minimum-capacity charge. CloudArq's AI-workload lens checks for exactly these — idle_sagemaker_endpoint, bedrock_provisioned_throughput_burn, and rag_vector_store_cost — from control-plane metadata only. See the AI-workload lens for the full picture.

product · detection

The CloudArq cost checks that catch this

CloudArq runs the inventory-plus-utilisation pass for you and flags each waste pattern automatically — read-only, with an impact estimate computed per resource from live AWS metadata and the exact remediation steps attached to every finding. These are 12 of the 192 checks CloudArq runs against an AWS account; there is no precomputed "total saved" — the estimate is always your account's real numbers.

idle_nat_gw
Idle NAT gateway
A NAT gateway billed hourly plus per-GB while moving almost no data — under ~1 GB over 30 days is a strong idle signal.
unattached_eip
Unattached Elastic IP
An allocated Elastic IP not associated with a running resource. AWS charges for idle EIPs, so release the ones you no longer need.
unused_ebs
Unattached EBS volume
A volume in the "available" state, attached to nothing, still billed for every provisioned GB. Snapshot it if you must keep it, then delete.
idle_elb
Idle load balancer
An ELB with essentially no traffic over 30 days still paying its hourly and LCU charges — a candidate to delete or consolidate.
gp2_to_gp3_ebs
gp2 volume cheaper as gp3
A general-purpose gp2 volume that would cost less per GB as gp3 at equivalent baseline performance — an in-place, no-downtime modify.
unused_ec2
Idle EC2 instance
An instance averaging under ~5% CPU over 14 days — either stop it or right-size it down.
oversized_ec2
Oversized EC2 instance
An instance whose sustained CPU and memory sit far below its size, so a smaller instance would carry the same load for less.
idle_rds
Idle RDS instance
A database with zero connections over 7 days still billing full compute and storage — pause, snapshot-and-delete, or downsize.
oversized_rds
Oversized RDS instance
An RDS instance with low CPU and memory utilisation relative to its class — a smaller instance class would serve the same workload.
no_savings_plan
No active Savings Plan on steady usage
Steady on-demand compute with no commitment in place — a Savings Plan on that baseline lowers the rate for the term.
ri_underutilization
Underutilised Reserved Instance
A Reserved Instance applying to under ~80% of eligible usage — you are paying for a commitment you are not fully consuming.
untagged_cost_owner
Untagged cost owner
A resource with no owner or cost-centre tag — waste with no name attached and invisible in cost-allocation reports.
findings · cost · idle & oversizedillustrative example
High1Medium2
idle_nat_gw
NAT gateway moved < 1 GB in 30 days
nat-0example · eu-north-1
fix →
oversized_ec2
Instance sustained 4% CPU for 14 days
i-0example · m5.2xlarge
fix →
unused_ebs
EBS volume unattached (available)
vol-0example · 500 GiB gp2
fix →

Each finding ships the copy-paste CLI / Terraform fix and a per-resource impact estimate — CloudArq is read-only and never changes anything itself.

faq

Frequently asked

01Where does most AWS cost waste actually hide?
In resources that bill by the hour whether or not anything uses them: idle NAT gateways, unattached Elastic IPs, unattached or oversized EBS volumes, stopped EC2 instances still paying for their disks, idle load balancers, and RDS or ElastiCache clusters with no traffic. None of these throw an error — they just quietly accrue on the bill — which is why an inventory-and-metrics pass finds more than staring at the Cost Explorer top line.
02Is switching gp2 volumes to gp3 safe, and worth it?
gp3 is the newer general-purpose SSD family that decouples IOPS and throughput from capacity, and AWS documents it as lower cost per GB than gp2 at the same baseline performance. The change is an in-place modify-volume with no downtime and no snapshot. Verify your workload does not depend on gp2 burst behaviour, set an explicit IOPS/throughput baseline, and modify during a low-traffic window.
03How do I right-size without guessing?
Use real utilisation, not intuition. AWS Compute Optimizer analyses CloudWatch history and recommends EC2, EBS, Lambda, and RDS resize targets; enable it and read the recommendations before you touch anything. For a quick manual check, pull 14 days of CPUUtilization from CloudWatch — sustained low CPU and memory is the signal to step down an instance family or size.
04Savings Plans or Reserved Instances — which should I buy?
Both trade a one- or three-year commitment for a lower rate on steady-state usage. Compute Savings Plans are the most flexible (they apply across instance family, size, region, and even Fargate/Lambda); Reserved Instances lock to more specific attributes. Buy commitments only for the baseline you are confident you will keep running for the term, and keep the rest on-demand. Check your existing commitments for underutilisation before buying more.
05How do I stop new waste from creeping back?
Two guardrails. First, cost-allocation tags: tag every resource with an owner and a cost centre so waste has a name attached and untagged spend is visible. Second, AWS Budgets: set a monthly budget with alert thresholds so a cost spike pages someone the day it happens, not at month-end. Budgets is alerting, not a hard stop — it warns; it does not cap your account.
06Does CloudArq turn things off to save money?
No. CloudArq is a read-only audit — it connects through a read-only IAM role with an ExternalId, never stores your credentials or data, and never auto-fixes anything. It detects idle, oversized, unattached, and uncommitted spend, estimates the impact per resource from live AWS metadata, and hands you the exact CLI or Terraform to remediate. You decide what to change and apply it yourself. There is no spend cap — detection plus the fix, never a hard stop.

Find every idle and oversized resource in one read-only pass

CloudArq connects through a read-only IAM role with an ExternalId, never stores your credentials or data, and never turns anything off. It surfaces idle, oversized, unattached, and uncommitted spend alongside the rest of your security and compliance posture — each with a per-resource impact estimate and the exact CLI or Terraform to remediate. Read the AWS cost optimization pillar or browse more AWS security & cost guides.