The fastest way to reduce an AWS bill is to work in order of effort, not in order of size. Ownership and alerts come first because they stop the bill from growing while you fix it. Then the cleanup: idle resources, NAT and data transfer, storage, logs. Commitments like Savings Plans come last, once your usage has stopped moving.
Each item below says where to look and how much work it usually is. Effort estimates are rough and assume a small team with one or two AWS accounts per environment.
Before you start: get one view of the bill
Open Cost Explorer, set the range to the last three full months, granularity monthly, and group by Service. Then switch the grouping to Usage type. The usage type view is where most surprises live, because it splits a service like EC2 into instance hours, EBS, data transfer and NAT.
If you spend more than a few thousand a month, also set up a Cost and Usage Report 2.0 export to S3 and query it with Athena. Cost Explorer answers "what"; the CUR with resource IDs enabled answers "which resource".
-- Athena over a CUR 2.0 export: top 20 usage types last month
SELECT line_item_product_code,
line_item_usage_type,
ROUND(SUM(line_item_unblended_cost), 2) AS cost
FROM cur2
WHERE line_item_usage_start_date >= date_trunc('month', current_date) - interval '1' month
AND line_item_usage_start_date < date_trunc('month', current_date)
GROUP BY 1, 2
ORDER BY cost DESC
LIMIT 20;
1. Tagging and ownership (half a day)
Every resource needs an owner, an environment and a service tag, and those tags need to be activated as cost allocation tags. Without them, every other item on this list turns into an argument about who is allowed to delete what.
Set tags once at the provider level in Terraform so nothing ships untagged:
provider "aws" {
region = "eu-central-1"
default_tags {
tags = {
owner = "team-payments"
environment = "prod"
service = "checkout"
}
}
}
Then activate the keys under Billing and Cost Management, Cost allocation tags. AWS notes that new tag keys can take up to 24 hours to appear and another 24 hours to activate, so do this on day one. Separate AWS accounts per environment, as described in our post on AWS Organizations from day one, give you a second, tamper-proof dimension for free.
2. Budgets and anomaly alerts (one hour)
Set a monthly budget with a forecast alert and an anomaly monitor per service. This does not save money directly, but it shortens the time between a mistake and someone noticing it from a month to a day.
AWS Budgets lets you monitor and receive notifications free of charge; only action-enabled budgets beyond the first two and emailed budget reports cost money.
resource "aws_budgets_budget" "monthly" {
name = "monthly-total"
budget_type = "COST"
limit_amount = "3000"
limit_unit = "USD"
time_unit = "MONTHLY"
notification {
comparison_operator = "GREATER_THAN"
threshold = 80
threshold_type = "PERCENTAGE"
notification_type = "FORECASTED"
subscriber_email_addresses = ["[email protected]"]
}
}
Add Cost Anomaly Detection with an AWS-managed monitor on the AWS services dimension and send individual alerts to an SNS topic that reaches Slack. Use a forecast alert, not an actual-spend one: by the time actual spend crosses 100%, the month is gone.
3. Idle and oversized resources (one to two days)
Delete what nobody uses, then shrink what is too big. This is usually the largest single quick win, and it is mostly detective work.
Where to look in Cost Explorer: group EC2 by Instance type and RDS by Usage type, then compare against CloudWatch CPU and memory. Typical finds:
- Unattached EBS volumes and old snapshots left behind by terminated instances.
- Load balancers with no healthy targets, and test environments nobody has opened in months.
- Idle public IPv4 addresses. AWS charges $0.005 per hour for every public IPv4 address, in use or idle.
- RDS instances sized for a launch-day load that never came.
- gp2 volumes. gp3 is listed at up to 20% lower price per GB with a 3,000 IOPS and 125 MB/s baseline at any size, and the change is online.
# Unattached EBS volumes
aws ec2 describe-volumes --filters Name=status,Values=available --query 'Volumes[].{id:VolumeId,gb:Size,type:VolumeType}' --output table
# Elastic IPs not associated with anything
aws ec2 describe-addresses --query 'Addresses[?AssociationId==null].PublicIp'
# Remaining gp2 volumes
aws ec2 describe-volumes --filters Name=volume-type,Values=gp2 --query 'Volumes[].VolumeId' --output text
Right-size in small steps, one instance size at a time, and watch latency for a week before the next step. A smaller instance that causes an incident costs more than it saves.
4. NAT gateways and data transfer (half a day to find, varies to fix)
If NatGateway-Bytes or DataTransfer usage types are in your top ten, you are probably paying to send traffic to AWS services through a NAT gateway. VPC gateway endpoints for S3 and DynamoDB are often the cheapest fix on this whole list.
Group by Usage type and filter on "NatGateway" and "DataTransfer". Common causes are container images pulled from ECR through NAT, logs shipped to a third party, and chatty cross-AZ traffic between services. We wrote up the detail, including how to trace which workload is responsible, in the hidden cost of AWS NAT gateways.
5. Storage classes and lifecycle rules (half a day)
S3 buckets that only grow should have lifecycle rules: transition old objects to cheaper classes, expire what you no longer need, and clean up incomplete multipart uploads and old versions.
Look at S3 grouped by Usage type (look for TimedStorage) and open S3 Storage Lens for per-bucket growth. For data with unpredictable access, S3 Intelligent-Tiering moves objects to cheaper tiers after 30 and 90 days without access, but objects under 128 KB are never tiered down, so it does little for buckets full of small files.
{
"Rules": [
{
"ID": "app-logs",
"Filter": { "Prefix": "logs/" },
"Status": "Enabled",
"Transitions": [
{ "Days": 30, "StorageClass": "STANDARD_IA" },
{ "Days": 90, "StorageClass": "GLACIER_IR" }
],
"Expiration": { "Days": 365 },
"NoncurrentVersionExpiration": { "NoncurrentDays": 30 },
"AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 7 }
}
]
}
Apply it with aws s3api put-bucket-lifecycle-configuration --bucket my-bucket --lifecycle-configuration file://lifecycle.json. Check retention requirements first; some logs must be kept for a defined period.
6. CloudWatch Logs retention (one hour)
Set a retention period on every log group. By default, CloudWatch Logs keeps data indefinitely, so storage grows every month whether anyone reads it or not.
# Set 30-day retention on every log group that has none
for lg in $(aws logs describe-log-groups --query 'logGroups[?!retentionInDays].logGroupName' --output text); do
aws logs put-retention-policy --log-group-name "$lg" --retention-in-days 30
done
Retention fixes storage, not ingestion. If DataProcessing-Bytes under CloudWatch is high, the fix is logging less: drop debug logs in production and sample noisy access logs. Choose retention with your compliance needs in mind; our GDPR logging guide covers what to keep and where.
7. Schedule non-production (half a day)
Development and staging environments rarely need to run nights and weekends. Stopping them outside working hours is one of the simplest savings available, because a week has 168 hours and a working week uses about 50 of them.
Filter Cost Explorer by your environment tag or by non-prod account. For ECS services, scale desired count to zero with a scheduled action; for EC2 and RDS, use the AWS Instance Scheduler solution or a small EventBridge-triggered Lambda. Note that a stopped RDS instance starts again automatically after seven days.
8. Spot for CI and non-production (one to three days)
CI runners, batch jobs and non-production workloads that tolerate interruption should run on Spot. AWS advertises savings of up to 90% versus On-Demand, in exchange for a two-minute interruption notice.
On ECS, Fargate Spot is a capacity provider you can mix with regular Fargate; AWS lists it at up to a 70% discount off the regular Fargate price. On EKS, use Karpenter or managed node groups with several instance types so one Spot pool running dry doesn't stall the cluster. Keep production on Spot only when the workload is stateless and you have tested interruptions.
9. Graviton (varies: hours for managed services, days for your own images)
Move to Arm-based Graviton instances where your software supports it. AWS states that Graviton-based instances cost up to 20% less than comparable x86 instances.
Start with managed services where the change is an instance class swap: RDS, ElastiCache and OpenSearch. For your own containers, build multi-arch images in CI with docker buildx build --platform linux/amd64,linux/arm64, run the test suite on Arm, then switch one service at a time. Native dependencies and old base images are the usual blockers.
10. Savings Plans and Reserved Instances (last, after usage stabilises)
Buy commitments only for the baseline you are sure will still be running in a year, and only after the cleanup above. Committing first locks in waste at a discount.
| Option | Covers | Flexibility | Listed max discount |
|---|---|---|---|
| Compute Savings Plan | EC2, Fargate, Lambda | Any family, size, Region, OS | Up to 66% |
| EC2 Instance Savings Plan | One instance family in one Region | Any size, AZ, OS within that family | Up to 72% |
| Database Savings Plan | RDS, Aurora, ElastiCache, DynamoDB and others | Any engine, family, size, Region; 1-year term | See pricing page |
| Reserved Instances | A specific database or instance configuration | Lowest | Varies by service |
For a startup, our default is a Compute Savings Plan for compute, because it survives a move from EC2 to Fargate, a Region change or a Graviton migration. Use Cost Explorer's Savings Plans recommendations with a 30 or 60-day lookback, commit to less than the recommendation, and check the coverage and utilisation reports monthly. Buying in smaller tranches every quarter beats one large purchase you might regret.
Make it stick
Savings fade unless someone looks at the bill on a schedule. Put a 30-minute cost review in the calendar once a month: top services, top changes month over month, untagged spend, and open anomaly alerts. Keep resource changes in Terraform, so a cleanup doesn't get undone by the next apply; how you organise Terraform state decides how easy that is.
If you'd rather have someone go through your accounts with this list and hand you a prioritised plan, that is the core of our cloud audit. We describe exactly what we check in the cloud audit we run.
Frequently asked questions
What is the fastest way to reduce an AWS bill at a startup?
Start with ownership and alerts, then delete idle resources: unattached EBS volumes, idle public IPv4 addresses, unused load balancers and forgotten test environments. Then fix NAT gateway traffic with VPC endpoints and set CloudWatch Logs retention. These usually take days, not weeks, and need no commitments.
Should a startup buy Savings Plans or Reserved Instances?
For compute, a Compute Savings Plan is usually the safer choice because it applies across EC2, Fargate and Lambda, any instance family and any Region. Buy it only after cleanup and once usage is stable, and commit to less than the Cost Explorer recommendation. Reserved Instances still fit fixed, long-lived database configurations.
How do I find what is driving my AWS costs?
In Cost Explorer, group by Service and then by Usage type for the last three months. Usage type exposes NAT gateway bytes, data transfer, EBS and log ingestion separately. For resource-level detail, export the Cost and Usage Report 2.0 to S3 with resource IDs enabled and query it with Athena.
Is Spot safe for production workloads?
Only for stateless workloads that can lose an instance with two minutes of notice and that you have tested under interruption. Spot fits CI runners, batch jobs and non-production best. For production, mix Spot with On-Demand capacity and spread across several instance types so one pool running out does not take you down.