Skip to content
Codeloom
AWS

AWS Cost Optimization: 10 Ways to Reduce Your Bill

Cut your AWS bill with 10 proven cost optimization strategies covering Reserved Instances, right-sizing, S3 tiers, spot instances, and more.

·9 min read · By Codeloom
Intermediate 16 min read

What you'll learn

  • Identify and eliminate wasted AWS spend
  • Choose between Reserved Instances, Savings Plans, and Spot
  • Right-size compute resources using CloudWatch data
  • Optimize storage costs with lifecycle policies and intelligent tiering

Prerequisites

  • Active AWS account with running workloads
  • Access to AWS Billing and Cost Explorer

AWS pricing is pay-as-you-go, but without active management, costs spiral quickly. Unused resources, oversized instances, and missed discount opportunities add up to thousands of dollars in waste every month. The good news is that most AWS accounts have 20-40% savings potential waiting to be captured.

Here are ten proven strategies to reduce your AWS bill, ordered from quick wins to longer-term optimizations.

1. Find and Terminate Unused Resources

The easiest savings come from resources you are paying for but not using. These accumulate over time as projects end, environments are abandoned, and experiments are forgotten.

Check for these common culprits:

# Unattached EBS volumes (you pay for these even when not mounted)
aws ec2 describe-volumes \
  --filters Name=status,Values=available \
  --query 'Volumes[].{ID:VolumeId,Size:Size,Type:VolumeType,Created:CreateTime}' \
  --output table

# Unused Elastic IPs (charged when not associated with a running instance)
aws ec2 describe-addresses \
  --query 'Addresses[?AssociationId==null].{IP:PublicIp,AllocationId:AllocationId}' \
  --output table

# Idle load balancers (no healthy targets)
aws elbv2 describe-target-health \
  --target-group-arn <target-group-arn> \
  --query 'TargetHealthDescriptions[?TargetHealth.State!=`healthy`]'

# Old snapshots (often forgotten after AMI cleanup)
aws ec2 describe-snapshots --owner-ids self \
  --query 'Snapshots[?StartTime<`2026-01-01`].{ID:SnapshotId,Size:VolumeSize,Date:StartTime}' \
  --output table

Set up a monthly review to catch newly idle resources. AWS Trusted Advisor (available with Business or Enterprise Support) automates some of these checks.

2. Right-Size Your Instances

Most EC2 instances are larger than they need to be. Developers often choose a size “just in case” and never revisit the decision. AWS Cost Explorer has a right-sizing recommendation feature that analyzes CloudWatch metrics:

# Get right-sizing recommendations
aws ce get-rightsizing-recommendation \
  --service EC2 \
  --configuration '{
    "RecommendationTarget": "SAME_INSTANCE_FAMILY",
    "BenefitsConsidered": true
  }'

Before downsizing, verify with CloudWatch metrics:

# Check CPU utilization over the past 2 weeks
aws cloudwatch get-metric-statistics \
  --namespace AWS/EC2 \
  --metric-name CPUUtilization \
  --dimensions Name=InstanceId,Value=i-1234567890abcdef0 \
  --start-time 2026-06-22T00:00:00Z \
  --end-time 2026-07-06T00:00:00Z \
  --period 3600 \
  --statistics Average Maximum \
  --query 'Datapoints | sort_by(@, &Timestamp) | [-48:]'

If the peak CPU stays under 40%, the instance is likely oversized. For memory-bound workloads, install the CloudWatch agent to collect memory metrics since these are not available by default.

Consider the newer instance families too. Graviton-based instances (like m7g, c7g, r7g) typically provide 20-30% better price-performance than their x86 equivalents.

3. Use Savings Plans or Reserved Instances

On-demand pricing is the most expensive way to run steady-state workloads. For predictable usage, Savings Plans and Reserved Instances offer 30-72% discounts.

Savings Plans are the simpler option. You commit to a consistent amount of compute spend (measured in dollars per hour) for 1 or 3 years:

  • Compute Savings Plans: Apply across EC2, Fargate, and Lambda regardless of region, instance family, or OS. Most flexible.
  • EC2 Instance Savings Plans: Locked to a specific instance family in a specific region. Deeper discounts.
# View Savings Plans recommendations
aws ce get-savings-plans-purchase-recommendation \
  --savings-plans-type COMPUTE_SP \
  --term-in-years ONE_YEAR \
  --payment-option NO_UPFRONT \
  --lookback-period-in-days SIXTY_DAYS

Reserved Instances are the older model, offering similar discounts but with less flexibility. They are still useful for RDS, ElastiCache, and Redshift where Savings Plans do not apply.

Start with a No Upfront, 1-year Compute Savings Plan covering your baseline usage. This is the lowest-risk commitment and still saves roughly 30%.

4. Leverage Spot Instances for Fault-Tolerant Workloads

Spot Instances use spare EC2 capacity at up to 90% discount. The tradeoff is that AWS can reclaim them with a 2-minute warning. They work well for:

  • Batch processing and data pipelines
  • CI/CD build agents
  • Stateless web servers behind an Auto Scaling Group
  • Machine learning training jobs
# Auto Scaling Group with mixed instances (spot + on-demand)
Resources:
  AutoScalingGroup:
    Type: AWS::AutoScaling::AutoScalingGroup
    Properties:
      MixedInstancesPolicy:
        InstancesDistribution:
          OnDemandBaseCapacity: 2
          OnDemandPercentageAboveBaseCapacity: 25
          SpotAllocationStrategy: capacity-optimized
        LaunchTemplate:
          LaunchTemplateSpecification:
            LaunchTemplateId: !Ref LaunchTemplate
            Version: !GetAtt LaunchTemplate.LatestVersionNumber
          Overrides:
            - InstanceType: m5.large
            - InstanceType: m5a.large
            - InstanceType: m5n.large
            - InstanceType: m4.large
      MinSize: 4
      MaxSize: 20

This configuration keeps 2 on-demand instances as a baseline and fills 75% of additional capacity with spot. Specifying multiple instance types improves availability since AWS can draw from multiple capacity pools.

5. Optimize S3 Storage Costs

S3 costs add up when you store large volumes of data in the wrong storage class. S3 offers several tiers:

Storage ClassUse CaseCost (per GB/month)
StandardFrequently accessed$0.023
Intelligent-TieringUnknown access patterns$0.023 + monitoring fee
Standard-IAInfrequent access (min 30 days)$0.0125
One Zone-IANon-critical infrequent data$0.01
Glacier Instant RetrievalArchive with instant access$0.004
Glacier FlexibleArchive, minutes to hours retrieval$0.0036
Glacier Deep ArchiveLong-term archive, 12-hour retrieval$0.00099

Set up lifecycle policies to automatically transition objects:

{
  "Rules": [
    {
      "ID": "ArchiveOldData",
      "Status": "Enabled",
      "Filter": {
        "Prefix": "logs/"
      },
      "Transitions": [
        {
          "Days": 30,
          "StorageClass": "STANDARD_IA"
        },
        {
          "Days": 90,
          "StorageClass": "GLACIER_IR"
        },
        {
          "Days": 365,
          "StorageClass": "DEEP_ARCHIVE"
        }
      ],
      "Expiration": {
        "Days": 2555
      }
    }
  ]
}
aws s3api put-bucket-lifecycle-configuration \
  --bucket my-bucket \
  --lifecycle-configuration file://lifecycle.json

For buckets with unpredictable access patterns, S3 Intelligent-Tiering automatically moves objects between tiers based on access frequency. The monitoring fee is $0.0025 per 1,000 objects per month, which pays for itself quickly on large datasets.

6. Use Compute Optimizer for Data-Driven Recommendations

AWS Compute Optimizer analyzes CloudWatch metrics and provides specific recommendations for EC2, EBS, Lambda, and ECS:

# Enable Compute Optimizer
aws compute-optimizer update-enrollment-status --status Active

# Get EC2 recommendations
aws compute-optimizer get-ec2-instance-recommendations \
  --query 'instanceRecommendations[].{
    Instance:instanceArn,
    Current:currentInstanceType,
    Recommended:recommendationOptions[0].instanceType,
    Savings:recommendationOptions[0].estimatedMonthlySavings.value
  }' --output table

Compute Optimizer is free and uses machine learning to analyze 14 days of metrics. It often catches right-sizing opportunities that manual analysis misses, especially for memory and network-bound workloads.

7. Optimize Data Transfer Costs

Data transfer is one of the most overlooked costs on AWS bills. Key rules:

  • Inbound traffic is free
  • Outbound to internet costs $0.09/GB (first 10 TB)
  • Cross-AZ traffic costs $0.01/GB each way
  • Cross-region traffic costs $0.02/GB

Reduce data transfer costs by:

# Use VPC endpoints for S3 (eliminates NAT Gateway data charges)
aws ec2 create-vpc-endpoint \
  --vpc-id vpc-12345 \
  --service-name com.amazonaws.us-east-1.s3 \
  --route-table-ids rtb-12345

# Use CloudFront for content delivery (often cheaper than direct S3)
# CloudFront data transfer is $0.085/GB vs $0.09/GB for S3 direct

For applications with heavy cross-AZ traffic, consider using placement groups or deploying single-AZ for non-critical workloads. Every GB that crosses an AZ boundary costs $0.02 round-trip.

8. Schedule Non-Production Resources

Dev and staging environments do not need to run 24/7. Shutting them down outside business hours saves 65% on those resources:

import boto3
from datetime import datetime

def lambda_handler(event, context):
    ec2 = boto3.client('ec2')
    action = event.get('action', 'stop')

    # Find instances tagged for scheduling
    filters = [
        {'Name': 'tag:Schedule', 'Values': ['business-hours']},
        {'Name': 'instance-state-name',
         'Values': ['running'] if action == 'stop' else ['stopped']}
    ]

    response = ec2.describe_instances(Filters=filters)
    instance_ids = []

    for reservation in response['Reservations']:
        for instance in reservation['Instances']:
            instance_ids.append(instance['InstanceId'])

    if not instance_ids:
        print(f"No instances to {action}")
        return

    if action == 'stop':
        ec2.stop_instances(InstanceIds=instance_ids)
        print(f"Stopped {len(instance_ids)} instances")
    else:
        ec2.start_instances(InstanceIds=instance_ids)
        print(f"Started {len(instance_ids)} instances")

Trigger this Lambda with two EventBridge rules: one to start at 8 AM and another to stop at 7 PM on weekdays. Do the same for RDS instances using stop-db-instance and start-db-instance.

9. Review and Optimize NAT Gateway Costs

NAT Gateways charge $0.045/hour plus $0.045/GB of data processed. For workloads that make heavy use of AWS services from private subnets, this adds up fast.

Mitigations:

  • VPC Gateway Endpoints for S3 and DynamoDB (free, eliminates NAT Gateway data charges for these services)
  • VPC Interface Endpoints for other AWS services (charges per hour and per GB, but often cheaper than NAT Gateway for high-volume services)
  • NAT instances for dev environments (a t3.micro NAT instance costs $7.50/month vs $32.40/month for a NAT Gateway)
# Check how much data your NAT Gateway processes
aws cloudwatch get-metric-statistics \
  --namespace AWS/NATGateway \
  --metric-name BytesOutToDestination \
  --dimensions Name=NatGatewayId,Value=nat-12345 \
  --start-time 2026-06-01T00:00:00Z \
  --end-time 2026-07-01T00:00:00Z \
  --period 2592000 \
  --statistics Sum

If you see tens or hundreds of GB going through NAT to S3, a gateway endpoint will pay for itself immediately since it is free.

10. Set Up Budgets and Alerts

Prevention is cheaper than remediation. AWS Budgets sends alerts when costs exceed thresholds:

aws budgets create-budget \
  --account-id 123456789012 \
  --budget '{
    "BudgetName": "MonthlyBudget",
    "BudgetLimit": {
      "Amount": "5000",
      "Unit": "USD"
    },
    "TimeUnit": "MONTHLY",
    "BudgetType": "COST"
  }' \
  --notifications-with-subscribers '[
    {
      "Notification": {
        "NotificationType": "ACTUAL",
        "ComparisonOperator": "GREATER_THAN",
        "Threshold": 80,
        "ThresholdType": "PERCENTAGE"
      },
      "Subscribers": [
        {
          "SubscriptionType": "EMAIL",
          "Address": "team@example.com"
        }
      ]
    },
    {
      "Notification": {
        "NotificationType": "FORECASTED",
        "ComparisonOperator": "GREATER_THAN",
        "Threshold": 100,
        "ThresholdType": "PERCENTAGE"
      },
      "Subscribers": [
        {
          "SubscriptionType": "EMAIL",
          "Address": "team@example.com"
        }
      ]
    }
  ]'

Create budgets per team or project using cost allocation tags. The forecasted alert is especially valuable since it warns you before the month ends, giving time to investigate and correct course.

Building a Cost Optimization Culture

Tools and automation are important, but lasting cost optimization requires organizational habits:

  • Tag everything. Enforce tagging with AWS Organizations SCPs. Without tags, you cannot attribute costs to teams, projects, or environments.
  • Review costs weekly. Use Cost Explorer’s daily granularity view to spot anomalies early.
  • Include cost in architecture reviews. Before deploying, estimate costs with the AWS Pricing Calculator. Include data transfer in the estimate.
  • Assign ownership. Every AWS account or workload should have a cost owner who reviews the bill and acts on recommendations.

Wrapping Up

AWS cost optimization is not a one-time project but an ongoing practice. Start with the quick wins: delete unused resources, right-size instances, and set up budgets. Then tackle the bigger opportunities like Savings Plans, spot instances, and storage lifecycle policies. The ten strategies in this guide can typically reduce an AWS bill by 30-50% without any architectural changes. Review your costs monthly, act on recommendations from Cost Explorer and Compute Optimizer, and make cost awareness part of your engineering culture.