The fastest way to reduce your AWS bill is to work in order of money saved per hour of effort: delete what nobody uses, rightsize the instances that are clearly oversized, move EBS volumes from gp2 to gp3 and to Graviton where you can, cover the steady baseline that remains with a Savings Plan, and then go after the quieter line items — S3 storage, data transfer, NAT Gateways and CloudWatch Logs. That order matters. Buy commitments before you rightsize and you lock in the waste for one or three years.
Below is the exact list I work through when someone hands me an account and asks me to cut the bill. Each item has the command or console path I actually use. Prices quoted are us-east-1 list prices at the time of writing; other regions are slightly higher.
Step 0: find where the money actually goes
Before touching anything, open Cost Explorer, set the range to the last three full months, granularity monthly, and group by Service. Then group by Usage type. The service view tells you which team to talk to; the usage type view tells you what is really being billed. A big “EC2-Other” line is the classic surprise: it is not instances at all, but EBS volumes, snapshots, NAT Gateway hours and bytes, and idle Elastic IPs.
If you prefer the CLI, this gives the same breakdown for the last month:
aws ce get-cost-and-usage \
--time-period Start=2026-08-01,End=2026-09-01 \
--granularity MONTHLY \
--metrics UnblendedCost \
--group-by Type=DIMENSION,Key=USAGE_TYPE \
--query 'ResultsByTime[0].Groups[?Metrics.UnblendedCost.Amount>`50`]' \
--output tableSort that list by cost. In almost every account I look at, the top ten usage types are 80% or more of the bill, so that is where the work goes.
The prioritized AWS cost optimization tips
Here is the order I use, with a rough sense of effort and risk. “Saving” is what the change typically removes from that specific line item, not from the whole bill.
| # | Fix | Typical saving on that line | Effort | Risk |
|---|---|---|---|---|
| 1 | Delete idle and orphaned resources | 100% of the waste | Low | Low (snapshot first) |
| 2 | Rightsize EC2 and RDS | Often 30-50% per resized instance | Medium | Medium |
| 3 | EBS gp2 to gp3 | About 20% per GB | Low | Very low |
| 4 | Graviton instances | Around 20% per hour | Medium | Medium (ARM builds) |
| 5 | Savings Plans or Reserved Instances | Up to 72% vs On-Demand | Low | Financial lock-in |
| 6 | S3 lifecycle rules | 45-95% on cold data | Low | Low |
| 7 | Data transfer and NAT Gateway | Varies widely | Medium | Low-medium |
| 8 | CloudWatch Logs retention | Storage for anything past retention | Very low | Low |
| 9 | Schedule non-production | Up to ~70% on those instances | Low | Low |
Savings Plans sit at number five on purpose. They are the biggest single lever, but they should be sized against the infrastructure you have after steps one to four, not before.
Delete idle resources first
This is free money and it needs no architecture discussion. The usual suspects:
- Unattached EBS volumes — left behind when an instance was terminated without “delete on termination”.
- Old snapshots and AMIs — especially from backup scripts that create but never prune.
- Idle Elastic IPs and public IPv4 addresses — AWS charges $0.005 per hour for every public IPv4 address, attached or not, which is about $3.65 a month each.
- Load balancers with no healthy targets — an ALB costs roughly $16 a month before it serves a single request.
- Forgotten RDS instances and dev environments from a proof of concept two years ago.
Finding unattached volumes across one region:
aws ec2 describe-volumes \
--filters Name=status,Values=available \
--query 'Volumes[].{ID:VolumeId,GB:Size,Type:VolumeType,Created:CreateTime}' \
--output tableMy rule: snapshot the volume, tag the snapshot with the date and a ticket number, delete the volume, and delete the snapshot after 30 days if nobody has shouted. Run the check in every region you have ever used — old experiments in eu-west-1 are a common find.
Rightsizing and Graviton
Rightsizing means matching instance size to real usage. Turn on AWS Compute Optimizer (it is free for the default 14-day lookback) and let it collect data. One important caveat: EC2 does not report memory usage by default, so without the CloudWatch agent installed, Compute Optimizer only sees CPU and may recommend a size that runs out of RAM. Install the agent on anything you plan to shrink.
What I look for: instances with p95 CPU under 20% and memory under 50% over two weeks are candidates for one size down. Drop one size at a time; this is routine work in EC2 management. An m5.2xlarge to an m5.xlarge halves the hourly cost; going straight to large is how you get paged at 2 a.m.
Move to Graviton where the stack allows it
Graviton (ARM) instances are around 20% cheaper per hour at list price than the equivalent Intel generation — for example m7g.large versus m7i.large — and for most web workloads they perform the same or better. Managed services are the easy wins: RDS, ElastiCache and OpenSearch can switch instance class with a modify and a reboot. For EC2 and containers, you need ARM builds. Most interpreted stacks (Node.js, Python, PHP, Ruby, Java) just work; native dependencies and old base images are what bite. I test on one instance behind the load balancer before switching the fleet.
Savings Plans vs Reserved Instances
Once the fleet is the right size, commit to the part of it that never goes away. The two options, compared:
| Compute Savings Plan | EC2 Instance Savings Plan | Reserved Instances | |
|---|---|---|---|
| Max discount (list) | Up to 66% | Up to 72% | Up to 72% |
| Covers | EC2, Fargate, Lambda | EC2 in one family and region | EC2, RDS, ElastiCache, OpenSearch, Redshift |
| Change instance family | Yes | No (size and OS yes) | Convertible RIs only |
| Change region | Yes | No | No |
My default: a Compute Savings Plan for EC2, Fargate and Lambda, because it survives rightsizing and the Graviton move, and Reserved Instances for RDS and ElastiCache, because Compute Savings Plans do not cover databases. AWS has since added Database Savings Plans, so compare their current terms against RIs for your engine before buying. Size the commitment to your usage trough — the lowest hourly spend over the last month — not the average. Undercommitting costs you a few percent; overcommitting means paying for hours you do not use. One-year, no-upfront is the sensible starting point for most businesses. The discount is smaller than three-year all-upfront, but so is the risk.
Lower AWS costs on storage: gp3 and S3 lifecycle
EBS gp2 to gp3
gp3 costs $0.08 per GB-month against $0.10 for gp2, and includes a baseline of 3,000 IOPS and 125 MB/s regardless of size. The change is online, with no detach and no downtime:
aws ec2 modify-volume --volume-id vol-0abc123def4567890 --volume-type gp3The one thing to check: gp2 volumes above 1 TB get more than 3,000 baseline IOPS, so for large gp2 volumes set --iops on gp3 to match what the workload actually uses. For everything under 1 TB, the default gp3 baseline is equal or better.
S3 lifecycle rules
S3 Standard is $0.023 per GB-month. Standard-IA is $0.0125, Glacier Instant Retrieval $0.004 and Glacier Deep Archive under $0.001. Logs, backups and old uploads rarely need Standard after 30 days. A lifecycle rule that transitions logs, expires them after a year and cleans up incomplete multipart uploads:
{
"Rules": [{
"ID": "logs-tiering",
"Filter": { "Prefix": "logs/" },
"Status": "Enabled",
"Transitions": [
{ "Days": 30, "StorageClass": "STANDARD_IA" },
{ "Days": 90, "StorageClass": "GLACIER_IR" }
],
"Expiration": { "Days": 365 },
"AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 7 }
}]
}Watch out for small objects: Standard-IA bills a minimum of 128 KB per object and 30 days of storage, so millions of tiny files can cost more after transition. For unpredictable access patterns, Intelligent-Tiering is the safer choice. I cover this in more depth as part of S3 storage optimization.
Data transfer, NAT Gateway and CloudWatch Logs
These are the line items people do not know they have. A NAT Gateway costs $0.045 per hour plus $0.045 for every GB it processes. If your private instances pull container images, OS updates or S3 objects through it, that per-GB charge adds up fast. An S3 gateway endpoint is free and removes S3 traffic from the NAT entirely. I wrote a full guide to cutting NAT Gateway cost because it is such a common finding.
Other transfer traps: traffic between Availability Zones is billed at $0.01 per GB in each direction, so a chatty app in one AZ talking to a database in another pays twice. Internet egress is $0.09 per GB after the first 100 GB a month; putting CloudFront in front of static assets and downloads usually lowers that.
CloudWatch Logs is the other one. Log groups default to never expire, ingestion is $0.50 per GB and storage $0.03 per GB-month. Set a retention on every group:
for g in $(aws logs describe-log-groups \
--query 'logGroups[?!retentionInDays].logGroupName' --output text); do
aws logs put-retention-policy --log-group-name "$g" --retention-in-days 30
doneRetention fixes storage; ingestion needs a change in the app. Debug-level logging left on in production is the most common cause of a large ingestion bill.
Keep it down: budgets, anomaly detection and schedules
Savings erode if nobody watches. The minimum I set up in every account:
- AWS Budgets with alerts at 80% and 100% of forecast spend, emailed to a person who will actually read them.
- Cost Anomaly Detection monitors per service, so a runaway Lambda loop or a log flood shows up in a day rather than on the invoice.
- Cost allocation tags (
env,team,project) activated in the Billing console, so the next conversation is about owners rather than guesses. - Schedules for non-production: staging that runs 12 hours on weekdays instead of 24/7 is billed for about 36% of the hours.
Guardrails are also the part to automate. If your infrastructure is in Terraform or CloudFormation, bake gp3 defaults, log retention and required tags into the modules so new resources start cheap instead of being fixed later.
When it is worth getting help
Everything above can be done in-house, and for a small account a free afternoon with Cost Explorer will catch most of it. It is worth bringing someone in when the bill is large enough that a few percent matters, when nobody on the team has time to own it, or when the fixes touch production networking and databases and you want them done carefully. That is exactly what my AWS cost optimization service covers: a read-only audit, a prioritized list with the saving and risk for each item, and the changes shipped for you. Small jobs are $20/hour; defined projects are a fixed price agreed in writing. If you would rather see what an audit would find first, start with a free AWS bill review.
Frequently asked questions
How much can I realistically save on AWS?
It depends on how the account was built. Accounts that have never been reviewed usually have obvious waste in idle resources, oversized instances and 100% On-Demand pricing. Accounts that already use Savings Plans and gp3 have less headroom. The only honest answer comes from looking at your Cost Explorer data by usage type.
Is AWS Trusted Advisor enough to cut costs?
It is a good starting list, but the full set of cost checks needs a Business or higher support plan, and it does not know your workloads. It will flag low-utilization instances, but it will not tell you whether a staging environment can be scheduled or whether traffic should bypass a NAT Gateway.
Do Savings Plans apply automatically?
Yes. Once purchased, a Savings Plan applies to eligible usage in your account (or across the organization, if sharing is on) every hour, highest-discount usage first. You do not assign it to specific instances.
Will rightsizing cause downtime?
Changing an EC2 instance type requires a stop and start, so a single instance will be briefly unavailable. Behind a load balancer or Auto Scaling group you can replace instances one at a time with no user-facing downtime. RDS class changes can be applied in the maintenance window, and Multi-AZ keeps the outage to a failover.