CloudAdvanced

AWS Cost Optimization: A Service-by-Service Playbook

Where AWS bills actually accumulate — EC2, S3, data transfer, NAT Gateways, and load balancers — and the specific levers for each, beyond generic right-sizing advice.

DevFieldGuideJuly 31, 20268 min read
Share:

Generic cloud cost advice ("right-size your instances") is true but not specific enough to act on. AWS bills have a handful of very particular shapes — NAT Gateway data processing charges, cross-AZ transfer, S3 request pricing — that don't show up until you know to look for them.

Start with the tools AWS already gives you for free

Before building anything custom, three built-in AWS tools surface real waste with zero setup:

  • Cost Explorer — breaks spend down by service, tag, and usage type, and its "Rightsizing Recommendations" report flags specific EC2 instances running well under capacity.
  • Compute Optimizer — analyzes actual CloudWatch utilization history (not just current config) and recommends specific instance type changes for EC2, EBS, Lambda, and ECS.
  • Trusted Advisor — flags idle load balancers, unattached EBS volumes, and low-utilization RDS instances, among other checks (some checks require Business or Enterprise support).
Prerequisites
  • An AWS account with Cost Explorer enabled (Billing console → Cost Explorer, enable once, no cost to enable).
  • IAM permissions to view billing/cost data (ce:* read actions) and, ideally, Compute Optimizer opted in at the account or organization level.

NAT Gateway: the charge most teams don't expect

A NAT Gateway bills two ways: an hourly charge, and a per-GB data processing charge for everything that flows through it. For a workload doing meaningful outbound traffic — pulling container images, calling external APIs, downloading dependencies during CI — that per-GB charge routinely dwarfs the hourly rate.

NAT Gateway: ~$0.045/hour + ~$0.045/GB processed (region-dependent, both ways)

The fix is architectural, not a setting to toggle: use VPC Gateway Endpoints for S3 and DynamoDB (free, and route that traffic without ever touching the NAT Gateway), and VPC Interface Endpoints for other AWS services you call frequently from private subnets. Traffic to AWS services routed through an endpoint instead of a NAT Gateway skips the per-GB NAT charge entirely.

bash
aws ec2 create-vpc-endpoint \
  --vpc-id vpc-0123456789abcdef0 \
  --service-name com.amazonaws.us-east-1.s3 \
  --route-table-ids rtb-0123456789abcdef0

Cross-AZ data transfer: the quiet architecture tax

Traffic between two resources in different Availability Zones within the same region is billed per GB in both directions. A chatty microservice architecture with services spread across AZs "for availability" can rack up meaningful cross-AZ charges just from normal internal traffic — this is separate from, and easy to conflate with, internet egress.

The practical mitigation isn't "put everything in one AZ" (that defeats the availability purpose) — it's being deliberate about what crosses AZs. High-volume internal chatter (a cache lookup on every request, for instance) benefits from AZ-aware routing or replicas in each AZ, so most traffic stays local; genuinely cross-AZ traffic (the minority, for actual failover/availability) is the acceptable remainder.

S3: request pricing adds up before storage does

S3 storage cost is usually not where the money goes for request-heavy workloads — request pricing is:

PUT/COPY/POST/LIST: ~$0.005 per 1,000 requests GET/SELECT: ~$0.0004 per 1,000 requests

Individually tiny, but a workload issuing millions of small GET requests (a poorly batched data pipeline, for instance) can generate a request bill larger than its storage bill. The fix is usually about access pattern, not price: batch small reads where possible, use S3 Select to filter server-side instead of downloading whole objects, and put CloudFront in front of frequently-read objects — CloudFront's own pricing plus a cache hit ratio that keeps repeat requests off S3 entirely.

Load balancers: pay for what's actually load-balancing

An Application Load Balancer or Network Load Balancer bills an hourly charge plus a usage-based LCU (Load Balancer Capacity Unit) charge, whether or not it's carrying meaningful traffic. Environments accumulate orphaned load balancers over time — a staging environment nobody tore down, a load balancer left behind after a service migration.

bash
# Find ALBs with zero healthy targets — likely orphaned
aws elbv2 describe-target-health --target-group-arn <arn>

Trusted Advisor's "Idle Load Balancers" check automates exactly this, but it's worth running manually across all regions periodically — an idle ALB in a region nobody checks doesn't show up in a routine dashboard scoped to your primary region.

EBS volumes: the classic orphan

Deleting an EC2 instance doesn't delete its attached EBS volumes unless DeleteOnTermination was set at launch (it defaults to true for the root volume, but not necessarily for additional attached volumes). Unattached volumes bill for storage indefinitely with nothing running on them.

bash
aws ec2 describe-volumes --filters Name=status,Values=available

Anything returned here is unattached and billing for nothing — the single highest-signal, lowest-effort cleanup check on this whole list.

Lambda and Fargate: cost mechanics that surprise teams moving off EC2

Serverless compute is usually cheaper for genuinely bursty or infrequent workloads, but it has its own specific cost shape worth understanding before assuming it's automatically cheaper for everything.

Lambda bills per invocation plus GB-seconds (memory allocated × execution duration). Increasing a function's memory allocation also increases its allocated CPU proportionally — a function that's CPU-bound, not just memory-bound, can sometimes get meaningfully cheaper by increasing memory, because the resulting faster execution time more than offsets the higher per-millisecond rate. This is counter-intuitive enough that it's worth actually testing with AWS Lambda Power Tuning rather than assuming the lowest memory setting is always cheapest.

Fargate bills per vCPU and GB of memory provisioned for the task's runtime, regardless of actual utilization — closer to EC2's billing model than Lambda's. A Fargate task provisioned generously "to be safe" pays for that headroom continuously, the same right-sizing problem as an over-provisioned EC2 instance, just billed per-second instead of per-instance-hour.

Lambda: billed only while actually executing, scales to zero Fargate: billed for the full task lifetime, doesn't scale to zero EC2: billed for the full instance lifetime, regardless of task count

The practical decision point: a workload that's idle most of the time and reacts to discrete events belongs on Lambda; a workload that runs continuously but needs container-level control (not full EC2 access) belongs on Fargate; a workload needing full OS control or specialized hardware belongs on EC2.

CloudWatch Logs: an easy-to-miss recurring cost

CloudWatch Logs bills for ingestion (per GB ingested) and storage (per GB per month, until a retention policy expires it) — and log groups have no expiration by default, meaning logs accumulate and bill indefinitely unless a retention period is explicitly set.

bash
aws logs put-retention-policy \
  --log-group-name /aws/lambda/my-function \
  --retention-in-days 30

A Lambda function logging verbosely at debug level in production, with no retention policy ever set on its log group, is a genuinely common source of a slowly climbing CloudWatch bill that nobody notices until a billing review — checking aws logs describe-log-groups for groups with retentionInDays: null (meaning "never expire") across an account is a fast, high-signal audit step.

Savings Plans vs. Reserved Instances

Both offer discounts for committed usage, but Savings Plans (Compute Savings Plans specifically) apply automatically across EC2, Fargate, and Lambda regardless of instance family or region changes, while EC2 Reserved Instances are tied to a specific instance family and region. For teams whose workload composition shifts over time — migrating instance types, moving workloads to Fargate — Compute Savings Plans' flexibility is usually worth the marginally lower discount ceiling compared to a Standard Reserved Instance locked to one exact configuration.

AspectReserved InstancesCompute Savings Plans
FlexibilityLocked to instance family + region (Standard)Applies across EC2, Fargate, Lambda automatically
Discount ceilingSlightly higher for exact-match usageSlightly lower, in exchange for flexibility
Best forA genuinely fixed, unchanging fleetWorkloads still evolving in shape

A practical audit order

Best practices
  1. Check Cost Explorer's Rightsizing Recommendations and Compute Optimizer first — zero setup, real data, usually the fastest wins.
  2. Look for unattached EBS volumes and idle/orphaned load balancers across all regions, not just your primary one.
  3. Check whether NAT Gateway data processing is a meaningful line item — if so, add VPC endpoints for S3/DynamoDB before anything else.
  4. Only then move to commitment-based savings (Savings Plans/RIs) — committing before you've cleaned up waste just locks in a discount on infrastructure you may not need at all.

Common mistakes

Common mistakes
  • Buying Reserved Instances or Savings Plans before auditing for waste. A discount on an idle resource is still money spent on something you didn't need — clean up first, commit second.
  • Assuming all data transfer costs the same. Same-AZ traffic is typically free, cross-AZ is billed, and internet egress is billed at a different (usually higher) rate — conflating them makes it impossible to diagnose which one is actually driving a spike.
  • Checking Trusted Advisor or Cost Explorer in only one region. Idle resources accumulate disproportionately in regions a team stopped actively using — a forgotten proof-of-concept region can quietly bill for months.
  • Setting DeleteOnTermination: false on additional EBS volumes without a deliberate reason, then terminating instances routinely. Every terminated instance leaves an orphaned, billing volume behind unless this is set correctly at launch.

Frequently Asked Questions

DevFieldGuide
DevFieldGuide

Editorial Team

Practical tutorials and developer tools, written and maintained by the DevFieldGuide team.

Enjoyed this article?

Get the next one straight to your inbox, along with the best of what we publish each week.

Related Articles

More in Cloud

View all