Skip to content
AWS aws cost 6 min read

Cost Optimization Strategies

Cutting an AWS bill is rarely about finding one clever discount. It is about working a short, ordered list: stop paying for things nobody uses, shrink the things that are too big, store data on cheaper tiers, and then commit to discounts for the steady “always-on” part of your usage. This page walks that list in priority order — the items near the top usually save the most money for the least effort. The recurring theme is waste: orphaned resources quietly bill you forever, so the single highest-leverage habit is sweeping for them on a schedule.

Where the money actually goes

Before changing anything, look at the bill. Open Cost Explorer (in the Billing and Cost Management console), group by Service, and set the range to the last 3 months. Almost every account follows the same pattern: a handful of services — usually EC2 (virtual servers), RDS (managed databases), S3 (object storage), and data transfer — make up the large majority of the cost. Fix those first; ignore the long tail of pennies.

Tip: The biggest wins are almost always waste elimination and right-sizing, not exotic discounts. A perfectly negotiated Savings Plan on an instance you didn’t need is still wasted money. Clean up first, commit second.

Step 1 — Kill idle and orphaned resources

Orphaned resources are leftovers that no longer attach to anything but keep billing: unattached EBS volumes, unused Elastic IPs, old snapshots and AMIs, and idle load balancers or NAT gateways. They are the quietest line items on the bill and the easiest savings you will ever find.

When to do this: always, and on a recurring schedule (monthly at minimum). There is no downside to removing something genuinely unused.

Unattached EBS volumes

An EBS (Elastic Block Store) volume is a virtual disk. When you terminate an instance, its extra data volumes can survive and keep charging — roughly $0.08/GB-month for gp3. A 500 GB orphan costs about $40/month for nothing.

Find every volume not attached to an instance:

aws ec2 describe-volumes \
  --filters Name=status,Values=available \
  --query "Volumes[].{ID:VolumeId,GB:Size,AZ:AvailabilityZone}" \
  --output table

Output:

-------------------------------------------------
|                 DescribeVolumes               |
+-----+-------------------------+----------------+
| GB  |          ID             |      AZ        |
+-----+-------------------------+----------------+
| 500 | vol-0a1b2c3d4e5f6a7b8   | us-east-1a     |
|  50 | vol-0f1e2d3c4b5a6978f   | us-east-1b     |
+-----+-------------------------+----------------+

Snapshot anything you might need, then delete: aws ec2 delete-volume --volume-id vol-0a1b2c3d4e5f6a7b8.

Unused Elastic IPs

An Elastic IP (a permanent public IP address you reserve) is free while attached to a running instance but costs about $0.005/hour (~$3.60/month) when allocated but not associated. Find the strays:

aws ec2 describe-addresses \
  --query "Addresses[?AssociationId==null].{IP:PublicIp,Alloc:AllocationId}" \
  --output table

Release one with aws ec2 release-address --allocation-id eipalloc-0a1b2c3d.

Idle load balancers, old snapshots, and unused AMIs

A load balancer with no healthy targets still costs ~$16+/month. Old EBS snapshots and the AMIs (Amazon Machine Images — server templates) built from them accumulate storage charges indefinitely. To clean these up in the console:

  1. Open the EC2 console.
  2. Under Load Balancers, sort by request count and delete any with zero traffic over the last month.
  3. Under Snapshots, sort by date and delete generations you no longer need for recovery.
  4. Under AMIs, deregister images you no longer deploy (this does not auto-delete their backing snapshots — delete those too).

Warning: A snapshot can be the only copy of important data, and an AMI may be needed to relaunch an Auto Scaling group. Confirm a resource is truly unused before deleting — tag a “keep” set so cleanup scripts skip them.

Step 2 — Right-size and move to Graviton

Right-sizing means matching instance size to real usage instead of a guess. Most instances are bought too large “to be safe” and then run at 10-20% CPU. AWS Compute Optimizer analyzes CloudWatch metrics and recommends a smaller (or differently shaped) instance.

aws compute-optimizer get-ec2-instance-recommendations \
  --query "instanceRecommendations[].{ID:instanceArn,Current:currentInstanceType,Finding:finding}" \
  --output table

Output:

----------------------------------------------------------------------
|                  GetEC2InstanceRecommendations                     |
+--------------+------------------+----------------------------------+
|   Current    |     Finding      |              ID                  |
+--------------+------------------+----------------------------------+
| m5.2xlarge   | Over-provisioned | ...:instance/i-0a1b2c3d4e5f      |
+--------------+------------------+----------------------------------+

Graviton is AWS’s own Arm-based processor. Graviton instances (the g suffix, e.g. m7g, c7g) typically deliver up to ~40% better price-performance than equivalent Intel/AMD instances. If your software runs on Arm (most interpreted languages, modern Java/Go/Node, and many containers do), switching is one of the largest single savings available.

OptionPrice-performanceWhen to use
Intel/AMD (e.g. m7i)BaselineSoftware with x86-only dependencies.
Graviton (e.g. m7g)Up to ~40% betterNew or recompilable workloads; default choice in 2026.
Right-size down (e.g. m5.2xlargem5.large)Pay for what you useAny instance flagged “over-provisioned.”

Step 3 — Optimize storage tiers

Storage is often half-priced with two simple moves.

  • EBS: move gp2 to gp3. gp3 is the current general-purpose SSD volume type. It is about 20% cheaper per GB than gp2 and lets you set throughput and IOPS (input/output operations per second) independently. Migrate live with no downtime: aws ec2 modify-volume --volume-id vol-0a1b2c3d4e5f6a7b8 --volume-type gp3.
  • S3: use lifecycle rules and Intelligent-Tiering. S3 Standard costs ~$0.023/GB-month; Glacier archive tiers cost a fraction of that. A lifecycle rule automatically moves or expires objects by age. S3 Intelligent-Tiering does it for you, shifting objects between access tiers based on usage with no retrieval fees on the frequent tiers.

A lifecycle rule that archives logs after 30 days and deletes them after 365:

{
  "Rules": [{
    "ID": "archive-then-expire-logs",
    "Filter": { "Prefix": "logs/" },
    "Status": "Enabled",
    "Transitions": [{ "Days": 30, "StorageClass": "GLACIER" }],
    "Expiration": { "Days": 365 }
  }]
}

Apply it: aws s3api put-bucket-lifecycle-configuration --bucket my-bucket --lifecycle-configuration file://lifecycle.json.

Step 4 — Commit to discounts for your baseline

Once waste is gone and instances are right-sized, you will see a steady “floor” of usage that runs 24/7. Buy a Savings Plan (a commitment to spend a fixed $/hour for 1 or 3 years) for that baseline only — it cuts compute cost by up to ~72% versus On-Demand. Cover the baseline, not the spiky top, so you never pay for unused commitment. Check what to buy in Cost Explorer → Savings Plans → Recommendations, which sizes the commitment from your actual history.

Step 5 — Cut data transfer and NAT costs

Data transfer is the bill nobody reads. Key rules:

  • Data out to the internet costs ~$0.09/GB; keep traffic inside AWS where possible and put a CDN (CloudFront) in front of public content to cut egress.
  • Cross-AZ traffic between Availability Zones costs ~$0.01/GB each way — it adds up for chatty services.
  • A NAT gateway (which lets private subnets reach the internet) charges ~$0.045/hour plus ~$0.045/GB processed. One idle-but-running NAT is ~$32/month before any data. Share one NAT per AZ instead of one per subnet, and use VPC gateway endpoints (free) for S3 and DynamoDB so that traffic skips the NAT entirely.

Best Practices

  • Sweep for orphaned resources (unattached volumes, idle EIPs, old snapshots/AMIs, idle LBs/NAT) on a monthly schedule — use Trusted Advisor and Cost Explorer to find them automatically.
  • Right-size with Compute Optimizer before buying any commitment; never lock in a discount on the wrong instance.
  • Default new workloads to Graviton and new volumes to gp3.
  • Put S3 lifecycle or Intelligent-Tiering on every bucket that holds logs, backups, or cold data.
  • Buy Savings Plans only for the always-on baseline; leave variable load On-Demand.
  • Treat data transfer as a first-class cost — add CloudFront for egress and gateway endpoints for S3/DynamoDB.
  • Tag everything so cost can be attributed, and review the top services in Cost Explorer every month.
Last updated June 15, 2026
Was this helpful?