Reduce Your AWS Bill: A 30-Day Plan and Why It's High

Why is your AWS bill so high? A 30-day plan to reduce your AWS bill: find the causes, delete waste, rightsize, commit, then set budgets and alerts.

When teams set out to reduce their AWS bill, it often goes the same way: someone panics after an invoice, deletes a few things, buys a Savings Plan the same afternoon and hopes. Sometimes it works. More often the bill dips for a month and creeps back, because nobody found out why it was high in the first place, and nothing stops it happening again.

This is the slower version, spread over four weeks. Week one is diagnosis. Weeks two and three take things away. Week four locks in what’s left and puts alarms around it. Each week has a read-only command or two you can run today. All prices are us-east-1 list prices at the time of writing.

If you want the short answer to how to reduce your AWS bill, the five biggest single levers are in our top 5 tips. This piece is about the order, the evidence and the guardrails that reduce AWS cost for good.

First, the uncomfortable part: there’s no off switch

For most accounts, AWS has no hard spending cap. AWS Budgets sends alerts, and budget actions can apply a deny policy or stop specific EC2 or RDS instances, but they work from data that’s hours old. AWS’s own docs say Budgets updates up to three times a day and that costs can keep growing after you’re notified. Storage keeps billing, and so do bytes already in flight.

There are two partial exceptions. Accounts on the newer free plan can’t be charged unless they upgrade. And AWS is rolling out project-level spend limits in its new AWS Settings experience, currently to a limited number of customers on paid plans. When a project hits its limit, AWS stops its resources, and if nobody acts within 90 days the project’s data is permanently deleted. That’s a real cap, with a real cost. If you run an existing account or an AWS Organization, plan as if neither applies.

Week 1: Work out why your AWS bill is so high

Don’t change anything this week. Write down what you find, with a monthly cost next to each item.

Where bill shock usually comes from

When people ask “why is my AWS bill so high”, the answer is rarely one dramatic resource. In the accounts we see, it’s usually one of these:

  • Credits ran out. The bill didn’t grow; it just stopped being paid by someone else.
  • A forgotten region. A demo in another region, still running, billing hourly.
  • EC2-Other. This line in Cost Explorer holds EBS volumes and snapshots, Elastic IPs, data transfer and NAT gateways, so it hides more surprises than any other.
  • NAT bytes. A NAT gateway is $0.045 an hour plus $0.045 for every GB it processes. A batch job pulling 20 TB a month from S3 through it costs about $920 in processing alone.
  • Quiet new charges. Public IPv4 addresses are $0.005 an hour each. Databases left on old engine versions pick up Extended Support fees.
  • One-time purchases. An All Upfront Reserved Instance is charged immediately, so it lands as one big day.

Start with last month by service, biggest first:

# Read-only: last month's top 10 services (GNU date, then BSD/macOS fallback)
START=$(date -u -d "$(date -u +%Y-%m-01) -1 month" +%F 2>/dev/null || date -u -v1d -v-1m +%F)
END=$(date -u +%Y-%m-01)
aws ce get-cost-and-usage \
  --time-period Start="$START",End="$END" \
  --granularity MONTHLY --metrics UnblendedCost \
  --group-by Type=DIMENSION,Key=SERVICE \
  --query "reverse(sort_by(ResultsByTime[0].Groups, &to_number(Metrics.UnblendedCost.Amount)))[:10].[Keys[0],Metrics.UnblendedCost.Amount]" \
  --output table

If EC2-Other is near the top, open it up by usage type. Names containing NatGateway-Bytes, EBS:SnapshotUsage or DataTransfer tell you which of the usual suspects you’re dealing with:

# Read-only: what's inside EC2-Other (reuses START and END from above)
aws ce get-cost-and-usage \
  --time-period Start="$START",End="$END" \
  --granularity MONTHLY --metrics UnblendedCost \
  --filter '{"Dimensions":{"Key":"SERVICE","Values":["EC2 - Other"]}}' \
  --group-by Type=DIMENSION,Key=USAGE_TYPE \
  --query "reverse(sort_by(ResultsByTime[0].Groups, &to_number(Metrics.UnblendedCost.Amount)))[:10].[Keys[0],Metrics.UnblendedCost.Amount]" \
  --output table

Each Cost Explorer API call costs $0.01, so run these by hand rather than in a loop.

When Cost Explorer looks wrong

Sometimes the “spike” is the tool, not the bill. A few things trip people up:

  • Cost Explorer refreshes at least once every 24 hours, and some data arrives later than that. Yesterday’s number is often incomplete, so a “drop” can be lag.
  • The current month is an estimate. The Bills page marks it Pending until the month closes and an invoice is issued.
  • The default unblended view puts upfront fees on the day you paid them. Switch to amortized cost to spread them across the term before you call it an anomaly.
  • Forecasts are a model. In our experience they’re shakiest in the first few days of a month and after a one-off spike.

Week 2: Delete what’s waste

Now you can take things away. Start with the cheapest decisions: things nobody owns.

Cost Optimization Hub, in the Billing and Cost Management console, gathers AWS’s recommendations across your accounts and regions and de-duplicates the savings estimate. Ask it for the “delete” and “stop” findings first:

# Read-only: idle-resource findings grouped by resource type
aws cost-optimization-hub list-recommendation-summaries \
  --region us-east-1 --group-by ResourceType \
  --filter '{"actionTypes":["Delete","Stop"]}' \
  --query "items[].[group,recommendationCount,estimatedMonthlySavings]" \
  --output table

Then sweep for running instances in regions you don’t think you use. This is how a forgotten demo gets found:

# Read-only: running instances in every enabled region
for r in $(aws ec2 describe-regions --query "Regions[].RegionName" --output text); do
  aws ec2 describe-instances --region "$r" \
    --filters Name=instance-state-name,Values=running \
    --query "Reservations[].Instances[].[InstanceId,InstanceType,LaunchTime]" \
    --output text | sed "s/^/$r  /"
done

When you’ve found an owner (or confirmed there isn’t one), stop it. Stopping isn’t deletion, but it loses anything on instance-store disks and usually changes the public IP, so confirm first:

REGION=eu-west-2; ID=i-0123456789abcdef0
aws ec2 describe-instances --region "$REGION" --instance-ids "$ID" \
  --query "Reservations[0].Instances[0].[InstanceType,Tags]" --output json
# CHANGES STATE: stops the instance. EBS volumes keep billing while it's stopped.
read -r -p "Stop $ID in $REGION? Type yes: " ok
[ "$ok" = yes ] && aws ec2 stop-instances --region "$REGION" --instance-ids "$ID"

Leave it stopped for a week or two before you terminate anything. If nobody shouts, snapshot and delete. Our guide to idle and orphaned AWS resources covers the rest of the basement: volumes, snapshots, load balancers and unused NAT gateways.

Week 3: Rightsize what’s left

What survived week two is real workload, probably oversized. Compute Optimizer looks at 14 days of CloudWatch metrics by default and gives a summary per resource type:

# Read-only: how many resources Compute Optimizer thinks are over-provisioned
aws compute-optimizer get-recommendation-summaries \
  --query "recommendationSummaries[].[recommendationResourceType,summaries[].[name,value]]" \
  --output json

A few rules keep this from causing an outage:

  • Look at peaks, not averages. A service at 8% average CPU might hit 70% every Monday morning.
  • Memory needs the CloudWatch agent. Without it, recommendations are based on CPU, network and disk alone.
  • One step at a time. Drop one size, watch for a week, then decide on the next.
  • Have the way back ready. For EC2 that’s a launch template version you can revert to.

This is also the week to look at Graviton, since you’re already testing new instance types. We cover that pairing in EC2 rightsizing and Graviton.

The trade-off is engineering time. Rightsizing a fleet of 200 instances is a project; rightsizing the five biggest is an afternoon. Sort by cost and stop when the savings stop being worth the meetings.

Week 4: Commit, then put up guardrails

Commit to the floor, not the average

Only now buy commitments. Look at your hourly compute spend since the clean-up and commit to the floor it rarely drops below. A one-year, no-upfront Compute Savings Plan is the cautious first purchase, and a second, smaller one in three months is better than one big guess today. Check what’s already covered before you buy more:

# Read-only: Savings Plans coverage over the last 30 days
START=$(date -u -d '-30 days' +%F 2>/dev/null || date -u -v-30d +%F)
END=$(date -u +%F)
aws ce get-savings-plans-coverage \
  --time-period Start="$START",End="$END" --granularity MONTHLY \
  --query "SavingsPlansCoverages[].Coverage.CoveragePercentage" --output text

Commitment sizing gets its own article: Reserved Instances vs Savings Plans.

Budgets and anomaly alerts

Budget monitoring and alerts are free; only action-enabled budgets past the first two cost $0.10 a day each. Set a monthly cost budget that alerts on the forecast, not just on actual spend, so you hear about trouble mid-month:

# WRITES: creates a budget (free, delete it any time). Amount and address are placeholders.
ACCOUNT=$(aws sts get-caller-identity --query Account --output text)
aws budgets create-budget --account-id "$ACCOUNT" \
  --budget '{"BudgetName":"monthly-total","BudgetLimit":{"Amount":"2000","Unit":"USD"},"TimeUnit":"MONTHLY","BudgetType":"COST"}' \
  --notifications-with-subscribers '[
    {"Notification":{"NotificationType":"FORECASTED","ComparisonOperator":"GREATER_THAN","Threshold":100,"ThresholdType":"PERCENTAGE"},
     "Subscribers":[{"SubscriptionType":"EMAIL","Address":"billing@example.com"}]},
    {"Notification":{"NotificationType":"ACTUAL","ComparisonOperator":"GREATER_THAN","Threshold":80,"ThresholdType":"PERCENTAGE"},
     "Subscribers":[{"SubscriptionType":"EMAIL","Address":"billing@example.com"}]}]'

Then check Cost Anomaly Detection. Standalone and payer accounts that enable Cost Explorer get a services monitor by default, with a daily email when an anomaly is at least $100 and 40% above expected. It runs about three times a day, needs ten days of history for a new service, and can take up to 24 hours to spot a spike. Lower the thresholds if $100 is a lot for you, and send the alerts somewhere a person reads them, not the root account’s inbox.

It’s good at sudden jumps. It won’t flag waste that’s been there since the day you launched, which is why weeks two and three come first.

If you’re a startup watching runway

Credits make AWS feel free, and that’s the trap. Your runway model should carry the gross bill, because that’s what you’ll pay the month the credits stop. Split last month by charge type to see it:

# Read-only: last month by record type, so credits show as their own line
START=$(date -u -d "$(date -u +%Y-%m-01) -1 month" +%F 2>/dev/null || date -u -v1d -v-1m +%F)
END=$(date -u +%Y-%m-01)
aws ce get-cost-and-usage --time-period Start="$START",End="$END" \
  --granularity MONTHLY --metrics UnblendedCost \
  --group-by Type=DIMENSION,Key=RECORD_TYPE --output table

If credits cover a $3,000 bill, your burn includes $3,000 of AWS whether or not you pay it today. Set your budget to exclude credits so the alerts track real usage. AWS Activate Founders starts at $1,000 in credits and Portfolio goes up to $200,000, but check each credit’s expiry date and terms, since credits usually don’t cover everything (AWS Marketplace purchases are a common exclusion).

For an early-stage company, the four weeks compress well. The two moves that matter most are usually a forecast budget on day one and deleting what the last prototype left behind.

A note for teams billed in India

AWS publishes its prices in US dollars, exclusive of GST. If your account is with AWS India (Amazon Internet Services Private Limited), invoices are issued in rupees, converted from those dollar amounts, with GST on top. So a budget set at $2,000 won’t match the INR total on the invoice. Expect the gap to move with the exchange rate.

Two practical points. Add your GST number under Tax Settings so it appears on the invoice and your finance team can claim input tax credit where eligible. And if your account is with AWS Inc. rather than AWS India, you’re usually invoiced in dollars; if you pay by card, check with your bank whether it adds a foreign-currency markup on card payments, because that’s a cost no AWS tool will show you.

FAQ

Can I set a hard spending limit on AWS?

For most accounts, no. Budgets alert and budget actions can restrict or stop some resources, but they react hours late. Project-level spend limits in AWS Settings are a true cap, but they’re in limited release and stop everything at the limit.

How much can I reduce my AWS cost in 30 days?

It depends on how much waste you start with. Weeks two and three usually move more than week four, and an account that’s never been cleaned up has more to give than one that has.

Is AWS cost reduction safe for production?

The read-only steps are. Stopping, deleting and resizing aren’t, so confirm an owner, keep a snapshot or rollback path, and change one thing at a time.

Why does Cost Explorer show a different number from my invoice?

The current month is an estimate until the invoice is issued, data can lag more than 24 hours, and upfront fees and credits move the totals. An India invoice also adds currency conversion and GST.

The first step to reduce your AWS bill

Today, run the first week-one command and write the top five services into a doc with a monthly cost and an owner next to each. That list is your plan for the month. If you’d like a second set of eyes on it, Kostrix runs a read-only audit of your AWS account, and the first 30 days are free.