Back to All Articles
#AWS#Cost Optimization#FinOps

Your Cloud Bill Is Lying to You

•8 min read•By Shuja Ahmed (DevOps Engineer)

Common AWS waste patterns: idle instances, over-provisioned RDS, forgotten EBS volumes, and NAT gateway costs. How to uncover them with real CLI commands.

When founders show me their AWS bills, they usually point to EC2 compute as the main culprit. They assume their developers wrote inefficient code or their servers are running hot. In eighty percent of the cloud cost audits I conduct, compute is not the problem.

The real problem is architectural waste: resources provisioned months ago for a sprint test, background data transfers routed through expensive gateways, storage volumes running attached to nothing, and log groups accumulating gigabytes of unexpired text.

As I analyzed in my guide to [ECS vs Kubernetes](/blog/ecs-vs-kubernetes-pick-the-boring-option), unmanaged control plane overhead and idle infrastructure are often the fastest routes to an inflated monthly bill.

Your monthly high-level invoice hides these details under generic line items. Here are the four biggest waste patterns I find in production AWS accounts, along with the exact commands and techniques to eliminate them.

#1. The NAT Gateway Highway Robbery

AWS charges $0.045 per hour for every Managed NAT Gateway you run in a VPC. That is roughly $32.40 per month per gateway before transferring a single byte. If you follow standard multi-AZ best practices across three availability zones, you have three NAT gateways costing you $97.20 monthly just to sit idle.

The real expense comes from data processing charges. AWS charges an additional $0.045 per gigabyte of data that passes through a NAT Gateway. If your backend containers pull a 2 GB Docker image from Docker Hub or push 500 GB of daily logs to an external service through the NAT gateway, you pay double: once for the gateway processing, and once for the internet egress.

The Fix: AWS VPC Gateway Endpoints

For any traffic destined for Amazon S3 or DynamoDB, configure a free VPC Gateway Endpoint. Traffic routes over AWS internal networking without touching the NAT gateway:

bash
# Create a free S3 VPC Gateway Endpoint using AWS CLI
aws ec2 create-vpc-endpoint \
  --vpc-id vpc-0a1b2c3d4e5f6g7h8 \
  --service-name com.amazonaws.us-east-1.s3 \
  --route-table-ids rtb-0123456789abcdef0

This single change once saved a client $640 a month whose background workers were reading media files from S3 through their NAT gateway.

#2. Orphaned EBS Volumes and Forgotten Snapshots

When you terminate an EC2 instance, AWS does not always delete the attached EBS volumes. If the volume was created without the DeleteOnTermination flag enabled, the volume stays in your account in the available state. AWS continues billing you for every gigabyte of provisioned gp3 or io2 storage every month indefinitely.

Run this AWS CLI command right now to find every unattached volume leaking money in your account:

bash
# List all unattached (orphan) EBS volumes
aws ec2 describe-volumes \
  --filters Name=status,Values=available \
  --query "Volumes[*].{ID:VolumeId,Size:Size,Type:VolumeType,Created:CreateTime}" \
  --output table

If an old 200 GB volume has been sitting available since 2024, create an automated snapshot if you need the data, and delete the volume immediately.

#3. CloudWatch Logs: The Silent Terabyte Accumulator

By default, when an AWS service creates a CloudWatch Log Group (for Lambda functions, ECS tasks, or API Gateway execution logs), AWS sets the retention policy to "Never Expire".

Over two years, verbose debug logs, health check pings, and database query logs accumulate quietly in S3 storage beneath CloudWatch. AWS bills $0.03 per GB per month for CloudWatch log storage. I recently audited an AWS account where an old staging Lambda function had generated 14 terabytes of unexpired logs over three years, costing the client $420 every single month for logs nobody had opened since 2023.

Here is an AWS CLI command to audit log groups with infinite retention and enforce a 14-day retention rule:

bash
# Audit log groups without expiration
aws logs describe-log-groups \
  --query "logGroups[?!not_null(retentionInDays)].logGroupName" \
  --output text

# Set retention to 14 days on a specific log group
aws logs put-retention-policy \
  --log-group-name "/aws/lambda/production-image-processor" \
  --retention-in-days 14

#4. Over-Provisioned Amazon RDS Databases

Most startups choose a database instance based on guesswork. A founder provisions a db.m6g.xlarge (4 vCPUs, 16 GB RAM) costing $280/month because they expect viral traffic. When we inspect AWS CloudWatch metrics over a 30-day window, the database average CPU utilization sits at 3.2%, and the free memory stays at 13 GB.

Furthermore, teams frequently keep Multi-AZ replication enabled in staging and testing environments. Multi-AZ doubles your database hourly cost. You do not need hot standby failover in a development database that your engineers use between 9 AM and 5 PM.

Actionable RDS Checklist:

  • Downgrade staging databases to db.t4g.micro or db.t4g.small.
  • Turn off Multi-AZ in non-production environments.
  • Enable RDS Storage Auto-scaling instead of pre-provisioning hundreds of gigabytes upfront.
  • Enable Amazon RDS Performance Insights to identify unindexed queries before buying bigger hardware.

#5. Graviton (ARM64) Migrations: Instant 20% Discount

If your workloads still run on Intel (x86_64) EC2 instance types such as t3, m5, or c5, you are paying a legacy tax. AWS Graviton processors (such as t4g, m7g, and c7g) offer roughly 20% lower hourly pricing and 15% to 25% better performance for modern workloads.

Because Docker, Node.js, Python, Go, and PostgreSQL compile and run natively on ARM64, switching is often as simple as changing an instance type string in your Terraform configuration:

hcl
# Change this:
# instance_type = "t3.medium" # $0.0416/hr

# To this:
instance_type = "t4g.medium" # $0.0336/hr (Immediate 19.2% compute savings)

Stop trusting the monthly total on your invoice. Audit your VPC endpoints, purge orphaned disks, enforce CloudWatch log retention, right-size your databases, and embrace ARM64 compute. You can almost always recover 25% to 50% of your current cloud spend without touching your application code.

SA
Shuja AhmedDevOps Engineer
More Articles
Direct Engineering Consultation

Need help implementing this in your own stack?

Book a complimentary 30-minute discovery call. We will audit your current bottlenecks, evaluate your cloud architecture, and give you concrete recommendations with zero sales pressure.