AWS Cost Optimization

By Vishvesh Patel · DevOps EngineerUpdated September 15, 2026

Where AWS bills actually go and how to reduce them. Savings Plans, Spot, Graviton, the hidden data transfer and NAT charges, and the tooling that finds waste.

Most AWS overspending is not exotic. It is idle resources nobody turned off, on-demand pricing for workloads that never change, oversized instances chosen by guesswork, and data transfer charges nobody knew existed.

The order below is roughly by payoff per hour of effort.

First, find out where the money goes

Optimising before measuring wastes effort on the wrong things.

Cost Explorer, grouped by service, then by usage type, answers most of it in ten minutes. Group by tag if your tagging is good enough, and if it is not, that is the first thing to fix, because untagged spend cannot be attributed to anyone who might reduce it.

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=SERVICE \
  --query 'ResultsByTime[0].Groups[?Metrics.UnblendedCost.Amount>`100`].[Keys[0],Metrics.UnblendedCost.Amount]' \
  --output table

Enable default_tags in Terraform or provider-level tagging so everything created carries an owner, environment and project. Retrofitting tags across an existing estate is tedious; doing it from the start costs nothing.

Set a budget with an alert before anything else. A billing surprise caught on day two is an annoyance; caught on day thirty it is a conversation with finance.

The big levers

Savings Plans

For any workload running continuously, on-demand pricing is the most expensive option available.

DiscountFlexibility
Compute Savings Plansup to ~66%Any instance family, size, region, and covers Fargate and Lambda
EC2 Instance Savings Plansup to ~72%Locked to a family in a region
Reserved Instancesup to ~72%Locked to attributes; can be sold on the marketplace

Compute Savings Plans are the pragmatic default. The extra few percent from the more rigid options is rarely worth losing the freedom to change instance family, which you will, when a newer generation arrives.

Commit to your baseline, not your peak. Cover the load that is always there and let bursts run on-demand or Spot. Over-committing is the one way a Savings Plan loses money.

Spot

Up to 90% off, and the largest single saving most teams leave untouched.

Spot capacity can be reclaimed with two minutes' notice, which rules out stateful singletons and rules in a great deal else: CI runners, batch processing, data pipelines, stateless web tiers behind a load balancer, and Kubernetes worker nodes.

The practical pattern is a mixed-instances policy. A base of on-demand capacity for reliability with Spot on top, diversified across several instance types so reclamation of one type does not take everything. On EKS, Karpenter handles this well.

Graviton

AWS's ARM processors are around 20% cheaper for equivalent performance, and often faster. If your workload is a container, an interpreted language, or anything with an ARM build, the migration is frequently a base image change and a rebuild.

It applies well beyond EC2. RDS, ElastiCache, OpenSearch and Lambda all offer Graviton options. Switching an RDS instance class to db.m6g is a maintenance-window change with an immediate permanent saving.

Newest generation

An m7i typically costs less than an m5 and performs better. Upgrading generation is usually a restart, and it is close to free money.

The charges people do not expect

This is where audits find surprises.

Data transfer out to the internet is often the largest single line on a mature bill. Serving through CloudFront is cheaper at volume and faster.

Cross-AZ traffic is charged in both directions. A chatty microservice architecture spread across three Availability Zones can spend meaningfully on traffic that never leaves the region. Topology-aware routing helps.

NAT gateway data processing charges per gigabyte, including for traffic to AWS services that never touches the internet. A gateway VPC endpoint for S3 and DynamoDB is free and removes that entirely, including ECR image pulls, since layers live in S3. This is a one-command change with an immediate effect.

CloudWatch Logs default to "Never expire." Logs accumulate and are billed forever. Setting retention on every log group is frequently the single largest avoidable line item.

aws logs describe-log-groups \
  --query 'logGroups[?!retentionInDays].[logGroupName,storedBytes]' --output table

Unattached EBS volumes and old snapshots keep billing after their instances are gone.

Idle load balancers cost an hourly charge whether or not anything is behind them.

Elastic IPs not attached to a running instance are charged.

S3 incomplete multipart uploads are billed as storage and do not appear in a normal listing.

# Unattached volumes
aws ec2 describe-volumes --filters Name=status,Values=available \
  --query 'Volumes[].[VolumeId,Size,CreateTime]' --output table

Turn things off

The least sophisticated lever and often the largest in percentage terms.

Non-production environments typically run 168 hours a week and are used for about 45. Stopping development instances, RDS databases and scaling Auto Scaling groups to zero outside working hours saves roughly 70% on those resources. AWS Instance Scheduler automates it.

Note RDS restarts a stopped instance automatically after seven days, so a scheduler rather than a manual stop is the reliable approach.

Storage tiering, carefully

S3 Intelligent-Tiering moves objects between access tiers automatically for a small monitoring fee, with no retrieval charges and no minimum duration penalties. It is the safe default when access patterns are unknown.

Manual lifecycle transitions to Standard-IA or Glacier are cheaper still, but carry minimum duration charges: 30 days for IA, 90 for most Glacier tiers, 180 for Deep Archive. Transitioning data that gets deleted after twenty days costs more than leaving it in Standard. Match the policy to the real lifecycle.

Tooling worth using

ToolWhat it does
Cost ExplorerWhere the money goes, by service, tag or usage type
AWS BudgetsAlerts before a surprise becomes a month
Compute OptimizerRight-sizing recommendations from real utilisation
Trusted AdvisorIdle resources, unattached volumes, low-utilisation instances
Cost Anomaly DetectionFlags unusual spend automatically
S3 Storage LensFree account-wide storage analytics

Compute Optimizer is the one to run first after Cost Explorer. It uses actual CloudWatch metrics rather than guesswork, and oversizing is close to universal.

A sensible order of work

  1. Tag everything, set a budget alert, open Cost Explorer.
  2. Set CloudWatch Logs retention on every log group.
  3. Add free S3 and DynamoDB gateway endpoints.
  4. Delete unattached volumes, old snapshots, idle load balancers, unassociated Elastic IPs.
  5. Schedule non-production shutdowns.
  6. Right-size from Compute Optimizer.
  7. Move to newest generation and Graviton.
  8. Buy Compute Savings Plans for the measured baseline.
  9. Move fault-tolerant workloads to Spot.

Steps 1 to 5 are quick and need no commitment. Steps 8 and 9 are the largest but want accurate measurement first, which is why they come last.

Quick reference

TaskCommand
Spend by serviceaws ce get-cost-and-usage --granularity MONTHLY --metrics UnblendedCost --group-by Type=DIMENSION,Key=SERVICE
Unattached volumesaws ec2 describe-volumes --filters Name=status,Values=available
Log groups with no retentionaws logs describe-log-groups --query 'logGroups[?!retentionInDays].logGroupName'
Set retentionaws logs put-retention-policy --log-group-name <n> --retention-in-days 30
Unassociated Elastic IPsaws ec2 describe-addresses --query 'Addresses[?!AssociationId]'
Orphaned S3 uploadsaws s3api list-multipart-uploads --bucket <b>
Savings Plan coverageaws ce get-savings-plans-coverage
Right-sizing adviceaws compute-optimizer get-ec2-instance-recommendations

Key takeaways

  • Measure before optimising. Cost Explorer grouped by service and usage type answers most of it quickly.
  • Compute Savings Plans on your baseline: flexible across family, size and region, and covers Fargate and Lambda.
  • Spot is the largest untapped saving for anything fault-tolerant.
  • Graviton is ~20% cheaper and often a base image change away.
  • CloudWatch Logs never expire by default: this is frequently the biggest avoidable line.
  • Free S3 and DynamoDB gateway endpoints remove NAT data processing charges.
  • Shut non-production down out of hours for roughly 70% off those resources.
  • IA and Glacier minimum durations can make tiering short-lived data more expensive, not less.

Check your understanding

Exercise

A line item nobody created

A team finds CloudWatch Logs is one of their largest AWS charges, though they never configured log storage. What is the most likely cause?

Exercise

Sizing a commitment

A workload runs 10 instances constantly and bursts to 25 during business hours. How much should a Compute Savings Plan cover?


Frequently Asked Questions

What is the fastest way to reduce an AWS bill?

Open Cost Explorer grouped by service first. Optimising before measuring wastes effort. Then the quick wins that need no commitment: set retention on CloudWatch log groups, which default to never expiring; add free S3 and DynamoDB gateway endpoints to remove NAT data processing charges; delete unattached EBS volumes, old snapshots, idle load balancers and unassociated Elastic IPs; and schedule non-production environments to shut down outside working hours, which saves around 70% on those resources.

What is the difference between Savings Plans and Reserved Instances?

Both discount steady usage in exchange for a one or three year commitment. Compute Savings Plans commit to an hourly dollar spend and apply across any instance family, size and region, and also cover Fargate and Lambda, so they keep applying as your architecture changes. Reserved Instances are tied to specific attributes and offer a slightly deeper discount, with the option to sell unused capacity on the marketplace. For most teams the flexibility of a Compute Savings Plan is worth the few percent difference.

Should I use Spot instances in production?

Yes, for anything fault-tolerant. Spot offers up to 90% off and is well suited to CI runners, batch jobs, data processing, stateless web tiers behind a load balancer, and Kubernetes worker nodes. Capacity can be reclaimed with two minutes' notice, so it is unsuitable for stateful singletons or anything that cannot be interrupted. Use a mixed-instances policy with an on-demand base and Spot diversified across several instance types, so reclamation of one type does not remove all your capacity.

Why is my AWS data transfer bill so high?

Three usual causes. Data transfer out to the internet is charged per gigabyte and is often the largest line. Serving through CloudFront is cheaper at volume. Cross-Availability-Zone traffic is charged in both directions, so a chatty architecture spread across three zones spends meaningfully on traffic that never leaves the region. And NAT gateway data processing charges per gigabyte even for traffic to AWS services, which free gateway endpoints for S3 and DynamoDB eliminate.

How much can Graviton actually save?

Around 20% for equivalent performance, and frequently better performance as well. It applies across EC2, RDS, ElastiCache, OpenSearch and Lambda, so the opportunity is broader than people assume. For containerised workloads and interpreted languages the migration is often just a base image change and a rebuild, making it one of the best returns on effort available. Compiled languages need an ARM build, which is usually straightforward but worth testing.

Is S3 Intelligent-Tiering worth it?

When access patterns are unknown or variable, yes. It moves objects between tiers automatically for a small per-object monitoring fee, with no retrieval charges and none of the minimum duration penalties that make manual tiering risky. If you know the access pattern precisely, explicit lifecycle rules to Standard-IA or Glacier are cheaper, but only when objects genuinely outlive the minimum duration, which is 30 days for IA, 90 for most Glacier tiers and 180 for Deep Archive. Transitioning data deleted after twenty days costs more than doing nothing.

Continue Learning

Explore Related Topics

When it breaks

Related Resources